Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function TimezoneSelect(props: TimezoneSelectProps) {
</div>
{value ? (
<TruncatedText className="text-foreground w-full min-w-0 flex-1 text-xs font-normal text-neutral-950">
{parseTimezone(value).label}
{parseTimezone(value)?.label ?? value}
</TruncatedText>
) : (
<TruncatedText className="w-full min-w-0 flex-1 text-xs font-normal text-neutral-400">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cronParser from 'cron-parser';
import { useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { RiInformation2Line } from 'react-icons/ri';
import { Hint, HintIcon } from '@/components/primitives/hint';
import { DaysOfWeek } from '@/components/workflow-editor/steps/digest-delay-tabs/days-of-week';
Expand Down Expand Up @@ -28,33 +28,43 @@ export const ScheduledType = ({
onValueChange: (cron: string) => void;
onError?: (error: unknown) => void;
}) => {
const period = useMemo(() => {
const onErrorRef = useRef(onError);
onErrorRef.current = onError;

const { period, periodError } = useMemo(() => {
try {
const cronParts = parseCronString(value);
return getPeriodFromCronParts(cronParts);

return { period: getPeriodFromCronParts(cronParts), periodError: null };
} catch (e) {
onError?.(e);
return PeriodValues.MINUTE;
return { period: PeriodValues.MINUTE, periodError: e };
}
}, [value, onError]);
}, [value]);

const { second, month, dayOfMonth, dayOfWeek, hour, minute } = useMemo(() => {
const { second, month, dayOfMonth, dayOfWeek, hour, minute, fieldsError } = useMemo(() => {
try {
const expression = cronParser.parseExpression(value);
return toUiFields(expression.fields);
} catch (e) {
onError?.(e);

return { ...toUiFields(expression.fields), fieldsError: null };
} catch (e) {
return {
second: [],
minute: [],
hour: [],
dayOfMonth: [],
month: [],
dayOfWeek: [],
fieldsError: e,
};
}
}, [value, onError]);
}, [value]);

useEffect(() => {
const error = periodError ?? fieldsError;
if (error) {
onErrorRef.current?.(error);
}
}, [periodError, fieldsError]);

const handleValueChange = (fields: Partial<UiCronFields>) => {
const cronFields = toCronFields({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ import {
isBridgeWorkflow,
ResourceOriginEnum,
ResourceTypeEnum,
slugify,
TriggerTypeEnum,
} from '@novu/shared';
import { PinoLogger } from 'nestjs-pino';
import { WorkflowWithPreferencesResponseDto } from '../../dtos/get-workflow-with-preferences.dto';
import { Instrument, InstrumentUsecase } from '../../instrumentation';
import { AnalyticsService, ContentService } from '../../services';
import { ResourceValidatorService } from '../../services/resource-validator.service';
import { isVariantEmpty, PlatformException, shortId } from '../../utils';
import { isVariantEmpty, PlatformException, shortId, slugifyOrRandom } from '../../utils';
import { MANAGE_TRANSLATIONS, TRANSLATIONS_SERVICE } from '../../utils/constants';
import { NotificationStep, NotificationStepVariantCommand } from '../../value-objects';
import { CreateChange, CreateChangeCommand } from '../create-change';
Expand Down Expand Up @@ -197,7 +196,7 @@ export class CreateWorkflowV0 {
* For non-bridge workflows, we use a slugified version of the workflow name
* as the trigger identifier to provide a better trigger DX.
*/
triggerIdentifier = slugify(command.name);
triggerIdentifier = slugifyOrRandom(command.name);
}

return triggerIdentifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
ResourceOriginEnum,
ResourceTypeEnum,
StepTypeEnum,
slugify,
WebhookEventEnum,
WebhookObjectTypeEnum,
WorkflowCreationSourceEnum,
Expand All @@ -28,7 +27,13 @@ import { WorkflowResponseDto } from '../../dtos/workflow/workflow-response.dto';
import { Instrument, InstrumentUsecase } from '../../instrumentation';
import { EmailControlType } from '../../schemas/control';
import { AnalyticsService } from '../../services';
import { computeWorkflowStatus, removeBrandingFromHtml, shortId, stepTypeToControlSchema } from '../../utils';
import {
computeWorkflowStatus,
removeBrandingFromHtml,
shortId,
slugifyOrRandom,
stepTypeToControlSchema,
} from '../../utils';
import { isStringifiedMailyJSONContent } from '../../utils/maily-utils';
import { isStepResolverActive } from '../../utils/step-resolver-control-state';
import { NotificationStep } from '../../value-objects';
Expand Down Expand Up @@ -155,7 +160,7 @@ export class UpsertWorkflowUseCase {
tags: workflowDto.tags || [],
userPreferences: workflowDto.preferences?.user ?? null,
defaultPreferences: workflowDto.preferences?.workflow ?? DEFAULT_WORKFLOW_PREFERENCES,
triggerIdentifier: preserveWorkflowId ? workflowDto.workflowId : slugify(workflowDto.name),
triggerIdentifier: preserveWorkflowId ? workflowDto.workflowId : slugifyOrRandom(workflowDto.name),
status: computeWorkflowStatus(isWorkflowActive, steps),
payloadSchema: workflowDto.payloadSchema,
validatePayload: workflowDto.validatePayload,
Expand Down Expand Up @@ -298,7 +303,7 @@ export class UpsertWorkflowUseCase {

@Instrument()
private generateUniqueStepId(step: UpsertStepDataCommand, previousSteps: NotificationStep[]): string {
const slug = slugify(step.name);
const slug = slugifyOrRandom(step.name);

let finalStepId = slug;
let attempts = 0;
Expand Down
5 changes: 3 additions & 2 deletions libs/application-generic/src/utils/build-slug.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ShortIsPrefixEnum, Slug, slugify } from '@novu/shared';
import { ShortIsPrefixEnum, Slug } from '@novu/shared';
import { encodeBase62 } from './base62';
import { slugifyOrRandom } from './slugify-or-random';

const SLUG_DELIMITER = '_';

Expand All @@ -8,5 +9,5 @@ const SLUG_DELIMITER = '_';
* @returns The slug for the entity, example: slug: "workflow-name_wf_AbC1Xyz9KlmNOpQr"
*/
export function buildSlug(entityName: string, shortIdPrefix: ShortIsPrefixEnum, internalId: string): Slug {
return `${slugify(entityName)}${SLUG_DELIMITER}${shortIdPrefix}${encodeBase62(internalId)}`;
return `${slugifyOrRandom(entityName)}${SLUG_DELIMITER}${shortIdPrefix}${encodeBase62(internalId)}`;
}
1 change: 1 addition & 0 deletions libs/application-generic/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './base62';
export * from './bridge';
export * from './build-slug';
export * from './build-variables';
export * from './slugify-or-random';
export * from './buildBridgeEndpointUrl';
export * from './compute-workflow-status';
export * from './create-schema';
Expand Down
14 changes: 14 additions & 0 deletions libs/application-generic/src/utils/slugify-or-random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { slugify } from '@novu/shared';
import { shortId } from './generate-id';

/**
* Returns slugify(name) if it produces a non-empty result,
* otherwise falls back to a random short ID.
* Handles names in non-Latin scripts (CJK, Arabic, etc.) whose characters
* are entirely stripped during transliteration.
*/
export function slugifyOrRandom(name: string): string {
const slug = slugify(name);

return slug || shortId();
}
10 changes: 10 additions & 0 deletions packages/shared/src/utils/slugify/slugify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,16 @@ describe('slugify', () => {
}
});

it('returns empty string for CJK-only input', () => {
expect(slugify('テストメール')).toBe('');
expect(slugify('你好世界')).toBe('');
expect(slugify('알림')).toBe('');
});

it('preserves ASCII parts in mixed-script input', () => {
expect(slugify('Test テスト')).toBe('test');
});

it('normalizes the string', () => {
const slug = decodeURIComponent('a%CC%8Aa%CC%88o%CC%88-123'); // åäö-123
expect(slugify(slug), 'aao-123');
Expand Down
Loading