From 0227e94d2d9bc38ec8d7043326419da2588041bc Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 16 Mar 2026 09:36:20 +0200 Subject: [PATCH 1/7] chore(worker, dashboard, api-service): Subscribers schedule feature flag (#10306) Co-authored-by: Cursor Agent --- .../src/app/events/e2e/trigger-event.e2e.ts | 5 - apps/api/src/app/inbox/e2e/session.e2e.ts | 42 ------ .../app/inbox/e2e/update-preferences.e2e.ts | 6 - .../inbox/usecases/session/session.usecase.ts | 11 -- .../subscribers/preferences/preferences.tsx | 33 ++-- .../steps/delay/delay-control-values.tsx | 16 +- .../digest-control-values.tsx | 16 +- apps/worker/src/.env.test | 1 - .../usecases/run-job/run-job.usecase.ts | 141 ++++++++---------- .../get-preferences.usecase.ts | 9 +- .../upsert-preferences.usecase.ts | 9 +- packages/shared/src/types/feature-flags.ts | 1 - 12 files changed, 92 insertions(+), 198 deletions(-) diff --git a/apps/api/src/app/events/e2e/trigger-event.e2e.ts b/apps/api/src/app/events/e2e/trigger-event.e2e.ts index 6df0f8e7ad9..df3bb7ac8b0 100644 --- a/apps/api/src/app/events/e2e/trigger-event.e2e.ts +++ b/apps/api/src/app/events/e2e/trigger-event.e2e.ts @@ -3998,18 +3998,13 @@ describe('Trigger event - /v1/events/trigger (POST) #novu-v2', () => { }); describe('Subscriber Schedule Logic', () => { - const isSubscribersScheduleEnabled = (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED; const isContextPreferencesEnabled = (process.env as Record).IS_CONTEXT_PREFERENCES_ENABLED; beforeEach(async () => { - // Enable the feature flags for schedule tests - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'true'; (process.env as Record).IS_CONTEXT_PREFERENCES_ENABLED = 'true'; }); afterEach(() => { - // Restore the original feature flag states - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = isSubscribersScheduleEnabled; (process.env as Record).IS_CONTEXT_PREFERENCES_ENABLED = isContextPreferencesEnabled; }); diff --git a/apps/api/src/app/inbox/e2e/session.e2e.ts b/apps/api/src/app/inbox/e2e/session.e2e.ts index 723552b5ab9..3e046c74f31 100644 --- a/apps/api/src/app/inbox/e2e/session.e2e.ts +++ b/apps/api/src/app/inbox/e2e/session.e2e.ts @@ -13,7 +13,6 @@ const mockSubscriberId = '12345'; describe('Session - /inbox/session (POST) #novu-v2', async () => { let session: UserSession; let subscriberRepository: SubscriberRepository; - const isSubscribersScheduleEnabled = (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED; before(async () => { subscriberRepository = new SubscriberRepository(); @@ -27,11 +26,6 @@ describe('Session - /inbox/session (POST) #novu-v2', async () => { _environmentId: session.environment._id, _organizationId: session.environment._organizationId, }); - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'true'; - }); - - afterEach(() => { - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = isSubscribersScheduleEnabled; }); const initializeSession = async ({ @@ -1133,42 +1127,6 @@ describe('Session - /inbox/session (POST) #novu-v2', async () => { expect(body.data.schedule.weeklySchedule.tuesday.hours[0].start).to.equal('09:00 AM'); }); - it('should not create schedule when feature flag is disabled', async () => { - // Disable the feature flag - - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'false'; - - await setIntegrationConfig({ - _environmentId: session.environment._id, - _organizationId: session.environment._organizationId, - hmac: false, - }); - - const defaultSchedule = { - isEnabled: true, - weeklySchedule: { - monday: { - isEnabled: true, - hours: [{ start: '09:00 AM', end: '05:00 PM' }], - }, - }, - }; - - const { body, status } = await initializeSession({ - applicationIdentifier: session.environment.identifier, - subscriberId: `feature-flag-disabled-${randomBytes(4).toString('hex')}`, - defaultSchedule, - }); - - expect(status).to.equal(201); - expect(body.data.token).to.be.ok; - expect(body.data.schedule).to.not.exist; - - // Re-enable the feature flag for other tests - - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'true'; - }); - it('should return context-specific schedule when multiple contexts exist', async () => { (process.env as any).IS_CONTEXT_PREFERENCES_ENABLED = 'true'; diff --git a/apps/api/src/app/inbox/e2e/update-preferences.e2e.ts b/apps/api/src/app/inbox/e2e/update-preferences.e2e.ts index e00e6632792..4dae8bf7a62 100644 --- a/apps/api/src/app/inbox/e2e/update-preferences.e2e.ts +++ b/apps/api/src/app/inbox/e2e/update-preferences.e2e.ts @@ -4,16 +4,10 @@ import { expect } from 'chai'; describe('Update global preferences - /inbox/preferences (PATCH) #novu-v2', () => { let session: UserSession; - const isSubscribersScheduleEnabled = (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED; beforeEach(async () => { session = new UserSession(); await session.initialize(); - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'true'; - }); - - afterEach(() => { - (process.env as Record).IS_SUBSCRIBERS_SCHEDULE_ENABLED = isSubscribersScheduleEnabled; }); it('should throw error when made unauthorized call', async () => { diff --git a/apps/api/src/app/inbox/usecases/session/session.usecase.ts b/apps/api/src/app/inbox/usecases/session/session.usecase.ts index a22251a2dc5..3da283783e9 100644 --- a/apps/api/src/app/inbox/usecases/session/session.usecase.ts +++ b/apps/api/src/app/inbox/usecases/session/session.usecase.ts @@ -300,17 +300,6 @@ export class Session { subscriber: SubscriberEntity; contextKeys: string[]; }): Promise { - const isSubscribersScheduleEnabled = await this.featureFlagsService.getFlag({ - key: FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED, - defaultValue: false, - environment: { _id: environment._id }, - organization: { _id: environment._organizationId }, - }); - - if (!isSubscribersScheduleEnabled) { - return undefined; - } - const schedule = await this.getSubscriberSchedule.execute( GetSubscriberScheduleCommand.create({ organizationId: environment._organizationId, diff --git a/apps/dashboard/src/components/subscribers/preferences/preferences.tsx b/apps/dashboard/src/components/subscribers/preferences/preferences.tsx index 5b292db17d6..b5105f6d390 100644 --- a/apps/dashboard/src/components/subscribers/preferences/preferences.tsx +++ b/apps/dashboard/src/components/subscribers/preferences/preferences.tsx @@ -41,7 +41,6 @@ export const Preferences = (props: PreferencesProps) => { }, }); - const isSubscribersScheduleEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED); const isContextPreferencesEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_CONTEXT_PREFERENCES_ENABLED); const { workflows, globalChannelsKeys, hasZeroPreferences } = useMemo(() => { @@ -109,24 +108,20 @@ export const Preferences = (props: PreferencesProps) => { - {isSubscribersScheduleEnabled && ( - <> - - -
- - - - - - - - - )} + + +
+ + + + + + +
diff --git a/apps/dashboard/src/components/workflow-editor/steps/delay/delay-control-values.tsx b/apps/dashboard/src/components/workflow-editor/steps/delay/delay-control-values.tsx index 0ed9a573bcc..f69364c472a 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/delay/delay-control-values.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/delay/delay-control-values.tsx @@ -1,9 +1,8 @@ -import { FeatureFlagsKeysEnum, UiComponentEnum, UiSchemaGroupEnum } from '@novu/shared'; +import { UiComponentEnum, UiSchemaGroupEnum } from '@novu/shared'; import { Separator } from '@/components/primitives/separator'; import { SidebarContent } from '@/components/side-navigation/sidebar'; import { getComponentByType } from '@/components/workflow-editor/steps/component-utils'; import { useWorkflow } from '@/components/workflow-editor/workflow-provider'; -import { useFeatureFlag } from '@/hooks/use-feature-flag'; const typeKey = 'type'; const amountKey = 'amount'; @@ -13,7 +12,6 @@ const dynamicKeyKey = 'dynamicKey'; const extendToScheduleKey = 'extendToSchedule'; export const DelayControlValues = () => { - const isSubscribersScheduleEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED); const { workflow, step } = useWorkflow(); const { uiSchema } = step?.controls ?? {}; @@ -40,14 +38,10 @@ export const DelayControlValues = () => { type?.component || amount?.component || unit?.component || cron?.component || dynamicKey?.component, })} - {isSubscribersScheduleEnabled && ( - <> - - - {getComponentByType({ component: extendToSchedule?.component ?? UiComponentEnum.EXTEND_TO_SCHEDULE })} - - - )} + + + {getComponentByType({ component: extendToSchedule?.component ?? UiComponentEnum.EXTEND_TO_SCHEDULE })} + )} diff --git a/apps/dashboard/src/components/workflow-editor/steps/digest-delay-tabs/digest-control-values.tsx b/apps/dashboard/src/components/workflow-editor/steps/digest-delay-tabs/digest-control-values.tsx index 3bcd3fce8cb..0518f4443dd 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/digest-delay-tabs/digest-control-values.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/digest-delay-tabs/digest-control-values.tsx @@ -1,14 +1,12 @@ -import { FeatureFlagsKeysEnum, UiComponentEnum, UiSchemaGroupEnum } from '@novu/shared'; +import { UiComponentEnum, UiSchemaGroupEnum } from '@novu/shared'; import { Separator } from '@/components/primitives/separator'; import { SidebarContent } from '@/components/side-navigation/sidebar'; import { getComponentByType } from '@/components/workflow-editor/steps/component-utils'; import { useWorkflow } from '@/components/workflow-editor/workflow-provider'; -import { useFeatureFlag } from '@/hooks/use-feature-flag'; const extendToScheduleKey = 'extendToSchedule'; export const DigestControlValues = () => { - const isSubscribersScheduleEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED); const { step } = useWorkflow(); const { uiSchema } = step?.controls ?? {}; @@ -43,14 +41,10 @@ export const DigestControlValues = () => { component: amount.component || unit.component || cron.component, })} - {isSubscribersScheduleEnabled && ( - <> - - - {getComponentByType({ component: extendToSchedule?.component ?? UiComponentEnum.EXTEND_TO_SCHEDULE })} - - - )} + + + {getComponentByType({ component: extendToSchedule?.component ?? UiComponentEnum.EXTEND_TO_SCHEDULE })} + )} diff --git a/apps/worker/src/.env.test b/apps/worker/src/.env.test index e027ba74b06..13a8ffaddaa 100644 --- a/apps/worker/src/.env.test +++ b/apps/worker/src/.env.test @@ -95,6 +95,5 @@ CLICK_HOUSE_DATABASE=test_logs IS_TRACE_LOGS_ENABLED=true IS_STEP_RUN_LOGS_WRITE_ENABLED=true IS_WORKFLOW_RUN_LOGS_WRITE_ENABLED=true -IS_SUBSCRIBERS_SCHEDULE_ENABLED=true IS_PUSH_UNREAD_COUNT_ENABLED=true IS_CONTEXT_PREFERENCES_ENABLED=true diff --git a/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts b/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts index 537930e3a9d..4b08a77467d 100644 --- a/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts @@ -136,13 +136,6 @@ export class RunJob { let notification: PartialNotificationEntity | null = null; try { - const isSubscribersScheduleEnabled = await this.featureFlagsService.getFlag({ - key: FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED, - defaultValue: false, - organization: { _id: job._organizationId }, - environment: { _id: job._environmentId }, - }); - notification = await this.notificationRepository.findOne( { _id: job._notificationId, @@ -180,86 +173,84 @@ export class RunJob { job.payload?.__source ); - if (isSubscribersScheduleEnabled) { - const schedule = await this.getSubscriberSchedule.execute( - GetSubscriberScheduleCommand.create({ - environmentId: job._environmentId, - organizationId: job._organizationId, - _subscriberId: job._subscriberId, - contextKeys: job.contextKeys, - }) - ); + const schedule = await this.getSubscriberSchedule.execute( + GetSubscriberScheduleCommand.create({ + environmentId: job._environmentId, + organizationId: job._organizationId, + _subscriberId: job._subscriberId, + contextKeys: job.contextKeys, + }) + ); - const subscriber = await this.subscriberRepository.findOne( + const subscriber = await this.subscriberRepository.findOne( + { + _id: job._subscriberId, + _environmentId: job._environmentId, + _organizationId: job._organizationId, + }, + 'timezone', + { readPreference: 'secondaryPreferred' } + ); + const timezone = subscriber?.timezone; + const isOutsideSubscriberSchedule = schedule?.isEnabled + ? !isWithinSchedule(schedule, new Date(), timezone) + : false; + + if ( + isOutsideSubscriberSchedule && + (await this.shouldExtendToSubscriberSchedule(job, notification.critical ?? false, workflow)) + ) { + this.logger.info( { - _id: job._subscriberId, - _environmentId: job._environmentId, - _organizationId: job._organizationId, + jobId: job._id, + subscriberId: job.subscriberId, + stepType: job.type, }, - 'timezone', - { readPreference: 'secondaryPreferred' } + "The step was extended to the next available time in the subscriber's schedule" ); - const timezone = subscriber?.timezone; - const isOutsideSubscriberSchedule = schedule?.isEnabled - ? !isWithinSchedule(schedule, new Date(), timezone) - : false; - - if ( - isOutsideSubscriberSchedule && - (await this.shouldExtendToSubscriberSchedule(job, notification.critical ?? false, workflow)) - ) { - this.logger.info( - { - jobId: job._id, - subscriberId: job.subscriberId, - stepType: job.type, - }, - "The step was extended to the next available time in the subscriber's schedule" - ); - isJobExtendedToSubscriberSchedule = await this.extendJobToNextAvailableSchedule(job, schedule, timezone); - if (isJobExtendedToSubscriberSchedule) { - shouldQueueNextJob = false; - return; - } + isJobExtendedToSubscriberSchedule = await this.extendJobToNextAvailableSchedule(job, schedule, timezone); + if (isJobExtendedToSubscriberSchedule) { + shouldQueueNextJob = false; + + return; } + } - if (isOutsideSubscriberSchedule && !this.shouldSkipScheduleCheck(job, notification.critical)) { - this.logger.info( - { - jobId: job._id, - subscriberId: job.subscriberId, - stepType: job.type, - }, - "The step was skipped as it fell outside the subscriber's schedule" - ); + if (isOutsideSubscriberSchedule && !this.shouldSkipScheduleCheck(job, notification.critical)) { + this.logger.info( + { + jobId: job._id, + subscriberId: job.subscriberId, + stepType: job.type, + }, + "The step was skipped as it fell outside the subscriber's schedule" + ); - await this.jobRepository.updateStatus(job._environmentId, job._id, JobStatusEnum.CANCELED); + await this.jobRepository.updateStatus(job._environmentId, job._id, JobStatusEnum.CANCELED); - await this.stepRunRepository.create(job, { - status: JobStatusEnum.CANCELED, - }); + await this.stepRunRepository.create(job, { + status: JobStatusEnum.CANCELED, + }); - await this.createExecutionDetails.execute( - CreateExecutionDetailsCommand.create({ - ...CreateExecutionDetailsCommand.getDetailsFromJob(job), - detail: DetailEnum.SKIPPED_STEP_OUTSIDE_OF_THE_SCHEDULE, - source: ExecutionDetailsSourceEnum.INTERNAL, - status: ExecutionDetailsStatusEnum.SUCCESS, - isTest: false, - isRetry: false, - raw: JSON.stringify({ - schedule, - timezone, - }), - }) - ); + await this.createExecutionDetails.execute( + CreateExecutionDetailsCommand.create({ + ...CreateExecutionDetailsCommand.getDetailsFromJob(job), + detail: DetailEnum.SKIPPED_STEP_OUTSIDE_OF_THE_SCHEDULE, + source: ExecutionDetailsSourceEnum.INTERNAL, + status: ExecutionDetailsStatusEnum.SUCCESS, + isTest: false, + isRetry: false, + raw: JSON.stringify({ + schedule, + timezone, + }), + }) + ); - // Update workflow run delivery lifecycle after schedule-based cancellation - await this.conditionallyUpdateDeliveryLifecycle(job, WorkflowRunStatusEnum.COMPLETED, workflow, notification); + await this.conditionallyUpdateDeliveryLifecycle(job, WorkflowRunStatusEnum.COMPLETED, workflow, notification); - return; - } + return; } await this.jobRepository.updateStatus(job._environmentId, job._id, JobStatusEnum.RUNNING); diff --git a/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts b/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts index 71083014507..320b01924bb 100644 --- a/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts +++ b/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts @@ -98,17 +98,10 @@ export class GetPreferences { }; } - const isSubscribersScheduleEnabled = await this.featureFlagsService.getFlag({ - key: FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED, - defaultValue: false, - environment: { _id: command.environmentId }, - organization: { _id: command.organizationId }, - }); - return { enabled: true, channels: GetPreferences.mapWorkflowPreferencesToChannelPreferences(result.preferences), - schedule: isSubscribersScheduleEnabled ? result.schedule : undefined, + schedule: result.schedule, }; } diff --git a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts index 10051b91701..dc6144f9ea4 100644 --- a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts +++ b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts @@ -60,13 +60,6 @@ export class UpsertPreferences { public async upsertSubscriberGlobalPreferences(command: UpsertSubscriberGlobalPreferencesCommand) { await this.deleteSubscriberWorkflowChannelPreferences(command); - const isSubscribersScheduleEnabled = await this.featureFlagsService.getFlag({ - key: FeatureFlagsKeysEnum.IS_SUBSCRIBERS_SCHEDULE_ENABLED, - defaultValue: false, - environment: { _id: command.environmentId }, - organization: { _id: command.organizationId }, - }); - return this.upsert({ _subscriberId: command._subscriberId, environmentId: command.environmentId, @@ -74,7 +67,7 @@ export class UpsertPreferences { preferences: command.preferences, type: PreferencesTypeEnum.SUBSCRIBER_GLOBAL, returnPreference: command.returnPreference, - schedule: isSubscribersScheduleEnabled ? command.schedule : undefined, + schedule: command.schedule, contextKeys: command.contextKeys, }); } diff --git a/packages/shared/src/types/feature-flags.ts b/packages/shared/src/types/feature-flags.ts index 87328ecbe87..15ee13f3919 100644 --- a/packages/shared/src/types/feature-flags.ts +++ b/packages/shared/src/types/feature-flags.ts @@ -63,7 +63,6 @@ export enum FeatureFlagsKeysEnum { IS_WORKFLOW_RUN_TREND_FROM_TRACES_ENABLED = 'IS_WORKFLOW_RUN_TREND_FROM_TRACES_ENABLED', IS_DELIVERY_LIFECYCLE_TRANSITION_ENABLED = 'IS_DELIVERY_LIFECYCLE_TRANSITION_ENABLED', IS_EXECUTION_DETAILS_CLICKHOUSE_ONLY_ENABLED = 'IS_EXECUTION_DETAILS_CLICKHOUSE_ONLY_ENABLED', - IS_SUBSCRIBERS_SCHEDULE_ENABLED = 'IS_SUBSCRIBERS_SCHEDULE_ENABLED', IS_GET_PREFERENCES_DISABLED = 'IS_GET_PREFERENCES_DISABLED', IS_REGION_SELECTOR_ENABLED = 'IS_REGION_SELECTOR_ENABLED', IS_PUSH_UNREAD_COUNT_ENABLED = 'IS_PUSH_UNREAD_COUNT_ENABLED', From b85341dc05d67be50085c66a60e00a9c23b5ea40 Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:24:49 +0200 Subject: [PATCH 2/7] fix(root): resolve high rollup and glob vulnerabilities (#10313) --- package.json | 5 ++- pnpm-lock.yaml | 87 +++++++++++++++++++------------------------------- 2 files changed, 37 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 31582eac776..ebac8cb9056 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,10 @@ "multer@<2.1.1": "^2.1.1", "jws@<3.2.3": "^3.2.3", "svgo@>=3.0.0 <3.3.3": "^3.3.3", - "flatted@<3.4.0": "^3.4.0" + "flatted@<3.4.0": "^3.4.0", + "rollup@>=3.0.0 <3.30.0": "^3.30.0", + "rollup@<2.80.0": "^2.80.0", + "glob@>=10.2.0 <10.5.0": "^10.5.0" }, "onlyBuiltDependencies": [ "@clerk/shared", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ffdcfaadf2..92ad86ad6ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ overrides: jws@<3.2.3: ^3.2.3 svgo@>=3.0.0 <3.3.3: ^3.3.3 flatted@<3.4.0: ^3.4.0 + rollup@>=3.0.0 <3.30.0: ^3.30.0 + rollup@<2.80.0: ^2.80.0 + glob@>=10.2.0 <10.5.0: ^10.5.0 importers: @@ -11803,13 +11806,13 @@ packages: resolution: {integrity: sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==} engines: {node: '>= 12.0.0'} peerDependencies: - rollup: ^2.68.0 + rollup: ^2.80.0 '@rollup/pluginutils@3.1.0': resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0 + rollup: ^2.80.0 '@rollup/pluginutils@5.1.2': resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} @@ -18740,10 +18743,6 @@ packages: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: '>=14'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -19078,14 +19077,8 @@ packages: resolution: {integrity: sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==} engines: {node: '>= 0.10'} - glob@10.4.2: - resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} - engines: {node: '>=16 || 14 >=14.18'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true @@ -25076,13 +25069,13 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup@2.79.2: - resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + rollup@2.80.0: + resolution: {integrity: sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==} engines: {node: '>=10.0.0'} hasBin: true - rollup@3.28.1: - resolution: {integrity: sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==} + rollup@3.30.0: + resolution: {integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true @@ -34199,7 +34192,7 @@ snapshots: chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit-x: 0.2.2 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 6.0.2 @@ -34930,7 +34923,7 @@ snapshots: cli-table3: 0.6.5 commander: 4.1.1 fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.15))(esbuild@0.27.3)) - glob: 10.4.2 + glob: 10.5.0 inquirer: 8.2.6 node-emoji: 1.11.0 ora: 5.4.1 @@ -40337,23 +40330,23 @@ snapshots: lodash: 4.17.21 lodash-es: 4.17.21 - '@rollup/plugin-commonjs@22.0.2(rollup@2.79.2)': + '@rollup/plugin-commonjs@22.0.2(rollup@2.80.0)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + '@rollup/pluginutils': 3.1.0(rollup@2.80.0) commondir: 1.0.1 estree-walker: 2.0.2 glob: 7.2.3 is-reference: 1.2.1 magic-string: 0.25.9 resolve: 1.22.8 - rollup: 2.79.2 + rollup: 2.80.0 - '@rollup/pluginutils@3.1.0(rollup@2.79.2)': + '@rollup/pluginutils@3.1.0(rollup@2.80.0)': dependencies: '@types/estree': 0.0.39 estree-walker: 1.0.1 picomatch: 2.3.1 - rollup: 2.79.2 + rollup: 2.80.0 '@rollup/pluginutils@5.1.2(rollup@4.59.0)': dependencies: @@ -42262,7 +42255,7 @@ snapshots: '@stoplight/spectral-ruleset-bundler@1.6.3(encoding@0.1.13)': dependencies: - '@rollup/plugin-commonjs': 22.0.2(rollup@2.79.2) + '@rollup/plugin-commonjs': 22.0.2(rollup@2.80.0) '@stoplight/path': 1.3.2 '@stoplight/spectral-core': 1.20.0(encoding@0.1.13) '@stoplight/spectral-formats': 1.8.2(encoding@0.1.13) @@ -42275,7 +42268,7 @@ snapshots: '@stoplight/types': 13.20.0 '@types/node': 22.15.13 pony-cause: 1.1.1 - rollup: 2.79.2 + rollup: 2.80.0 tslib: 2.8.1 validate-npm-package-name: 3.0.0 transitivePeerDependencies: @@ -46500,7 +46493,7 @@ snapshots: code-red@1.0.4: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 acorn: 8.15.0 estree-walker: 3.0.3 periscopic: 3.1.0 @@ -49199,11 +49192,6 @@ snapshots: cross-spawn: 7.0.5 signal-exit: 3.0.7 - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.5 - signal-exit: 4.1.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -49617,18 +49605,9 @@ snapshots: dependencies: find-index: 0.1.1 - glob@10.4.2: - dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.0 - path-scurry: 1.11.1 - - glob@10.4.5: + glob@10.5.0: dependencies: - foreground-child: 3.3.0 + foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -50866,7 +50845,7 @@ snapshots: is-reference@3.0.2: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 is-regex@1.1.4: dependencies: @@ -51287,7 +51266,7 @@ snapshots: chalk: 4.1.2 ci-info: 4.3.0 deepmerge: 4.3.1 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 jest-circus: 30.0.5(babel-plugin-macros@3.1.0) jest-docblock: 30.0.1 @@ -51813,7 +51792,7 @@ snapshots: chalk: 4.1.2 cjs-module-lexer: 2.1.0 collect-v8-coverage: 1.0.2 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 jest-haste-map: 30.0.5 jest-message-util: 30.0.5 @@ -55454,7 +55433,7 @@ snapshots: periscopic@3.1.0: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 estree-walker: 3.0.3 is-reference: 3.0.2 @@ -57473,17 +57452,17 @@ snapshots: rimraf@5.0.10: dependencies: - glob: 10.4.5 + glob: 10.5.0 ringbufferjs@2.0.0: {} robust-predicates@3.0.2: {} - rollup@2.79.2: + rollup@2.80.0: optionalDependencies: fsevents: 2.3.3 - rollup@3.28.1: + rollup@3.30.0: optionalDependencies: fsevents: 2.3.3 optional: true @@ -58730,7 +58709,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.4.5 + glob: 10.5.0 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 @@ -58847,7 +58826,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.30 - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.0 axobject-query: 4.1.0 @@ -60529,7 +60508,7 @@ snapshots: dependencies: esbuild: 0.18.20 postcss: 8.4.47 - rollup: 3.28.1 + rollup: 3.30.0 optionalDependencies: '@types/node': 22.15.13 fsevents: 2.3.3 From 7a1ec25131642288e9d2558d21fcb62c69420f8b Mon Sep 17 00:00:00 2001 From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:04:42 +0200 Subject: [PATCH 3/7] fix(providers): include novu message ID in payload data for FCM notifications (#10310) --- packages/providers/src/lib/push/fcm/fcm.provider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/providers/src/lib/push/fcm/fcm.provider.ts b/packages/providers/src/lib/push/fcm/fcm.provider.ts index d698ba8debf..1a0f1ecbe90 100644 --- a/packages/providers/src/lib/push/fcm/fcm.provider.ts +++ b/packages/providers/src/lib/push/fcm/fcm.provider.ts @@ -57,6 +57,7 @@ export class FcmPushProvider extends BaseProvider implements IPushProvider { }) || {}; const payload = this.cleanPayload(options.payload); + const novuData = payload.__nvMessageId ? { __nvMessageId: payload.__nvMessageId } : {}; const transformedBase = this.transform(bridgeProviderData, {}); const commonProps: Partial = { @@ -75,7 +76,7 @@ export class FcmPushProvider extends BaseProvider implements IPushProvider { title: options.title, body: options.content, }, - data, + data: { ...novuData, ...data }, ...commonProps, }).body; @@ -100,7 +101,7 @@ export class FcmPushProvider extends BaseProvider implements IPushProvider { body: options.content, ...overridesData, }; - multicastConfig.data = data; + multicastConfig.data = { ...novuData, ...data }; } const multicastMessage = this.transform( From 21408707d55ee7cf5d921655343baee19823a80f Mon Sep 17 00:00:00 2001 From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:07:05 +0200 Subject: [PATCH 4/7] fix(api-service): monthly usage url button (#10315) --- .source | 2 +- .../src/workflows/usage-report/email.tsx | 23 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.source b/.source index 75e8eeb78da..5e1ec389960 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit 75e8eeb78da6d8b8b2403172666cca55029523c4 +Subproject commit 5e1ec389960b82beea28002a132c3534832c4dfa diff --git a/libs/notifications/src/workflows/usage-report/email.tsx b/libs/notifications/src/workflows/usage-report/email.tsx index fb4965acc05..c6a850d87d0 100644 --- a/libs/notifications/src/workflows/usage-report/email.tsx +++ b/libs/notifications/src/workflows/usage-report/email.tsx @@ -1,18 +1,5 @@ import { providers as sharedProviders } from '@novu/shared'; -import { - Body, - Button, - Column, - Container, - Head, - Html, - Img, - Link, - Preview, - Row, - render, - Section, -} from '@react-email/components'; +import { Body, Column, Container, Head, Html, Img, Link, Preview, Row, render, Section } from '@react-email/components'; import millify from 'millify'; import React from 'react'; import { ControlValueSchema, PayloadSchemaType } from './schemas'; @@ -905,15 +892,17 @@ function FooterCta({ dashboardUrl }: { dashboardUrl: string }) { - + From b674dd6c341d55ea9161b60f4352ceea07dc3b9d Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:28:57 +0200 Subject: [PATCH 5/7] fix(root): resolve high undici and moderate jose vulnerabilities (#10316) Co-authored-by: Cursor Agent Co-authored-by: Dima Grossman --- package.json | 5 ++++- pnpm-lock.yaml | 38 +++++++++++++++----------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index ebac8cb9056..00532f48d4c 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,10 @@ "flatted@<3.4.0": "^3.4.0", "rollup@>=3.0.0 <3.30.0": "^3.30.0", "rollup@<2.80.0": "^2.80.0", - "glob@>=10.2.0 <10.5.0": "^10.5.0" + "glob@>=10.2.0 <10.5.0": "^10.5.0", + "undici@>=7.0.0 <7.24.0": "^7.24.0", + "undici@>=6.0.0 <6.24.0": "^6.24.0", + "jose@>=3.0.0 <4.15.5": "^4.15.5" }, "onlyBuiltDependencies": [ "@clerk/shared", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92ad86ad6ce..c8f64f5b49b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ overrides: rollup@>=3.0.0 <3.30.0: ^3.30.0 rollup@<2.80.0: ^2.80.0 glob@>=10.2.0 <10.5.0: ^10.5.0 + undici@>=7.0.0 <7.24.0: ^7.24.0 + undici@>=6.0.0 <6.24.0: ^6.24.0 + jose@>=3.0.0 <4.15.5: ^4.15.5 importers: @@ -20681,9 +20684,6 @@ packages: joi@17.11.0: resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} - jose@4.13.1: - resolution: {integrity: sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==} - jose@4.15.5: resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} @@ -22428,7 +22428,7 @@ packages: native-fetch@4.0.2: resolution: {integrity: sha512-4QcVlKFtv2EYVS5MBgsGX5+NWKtbDbIECdUXDBGDMAZXq3Jkv9zf+y8iS7Ub8fEdga3GpYeazp9gauNqXHJOCg==} peerDependencies: - undici: '*' + undici: ^7.24.0 natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -26979,16 +26979,12 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} - undici@6.19.8: - resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} + undici@6.24.1: + resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} - engines: {node: '>=20.18.1'} - - undici@7.21.0: - resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + undici@7.24.3: + resolution: {integrity: sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -35189,7 +35185,7 @@ snapshots: request-ip: 3.3.0 ringbufferjs: 2.0.0 semver: 7.7.3 - undici: 7.21.0 + undici: 7.24.3 unescape: 1.0.1 unescape-js: 1.1.4 uuid: 9.0.1 @@ -35605,7 +35601,7 @@ snapshots: '@octokit/plugin-paginate-rest': 9.2.1(@octokit/core@5.2.0) '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.0) '@octokit/types': 12.6.0 - undici: 6.19.8 + undici: 6.24.1 '@octokit/auth-action@4.1.0': dependencies: @@ -46240,7 +46236,7 @@ snapshots: parse5: 7.1.2 parse5-htmlparser2-tree-adapter: 7.0.0 parse5-parser-stream: 7.1.2 - undici: 6.19.8 + undici: 6.24.1 whatwg-mimetype: 4.0.0 cheerio@1.0.0-rc.12: @@ -52056,8 +52052,6 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@4.13.1: {} - jose@4.15.5: {} jose@5.10.0: {} @@ -53501,7 +53495,7 @@ snapshots: messagebird@4.0.1: dependencies: - jose: 4.13.1 + jose: 4.15.5 safe-buffer: 5.2.1 scmp: 2.1.0 @@ -53846,7 +53840,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.18.2 + undici: 7.24.3 workerd: 1.20260302.0 ws: 8.18.0 youch: 4.1.0-beta.10 @@ -59929,11 +59923,9 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - undici@6.19.8: {} - - undici@7.18.2: {} + undici@6.24.1: {} - undici@7.21.0: {} + undici@7.24.3: {} unenv@2.0.0-rc.24: dependencies: From ac049e2bb4dd7681a8ae5f9803df44fbceec8ef1 Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:23:45 +0200 Subject: [PATCH 6/7] fix(api-service): handle null notification after update and validate ObjectIds in markAs (#10318) Co-authored-by: Cursor Agent Co-authored-by: Dima Grossman --- .../mark-notification-as.usecase.ts | 17 +++++++++++------ apps/api/src/app/widgets/widgets.controller.ts | 5 +++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/api/src/app/inbox/usecases/mark-notification-as/mark-notification-as.usecase.ts b/apps/api/src/app/inbox/usecases/mark-notification-as/mark-notification-as.usecase.ts index 7a8005609e2..76e3ca385c7 100644 --- a/apps/api/src/app/inbox/usecases/mark-notification-as/mark-notification-as.usecase.ts +++ b/apps/api/src/app/inbox/usecases/mark-notification-as/mark-notification-as.usecase.ts @@ -61,11 +61,16 @@ export class MarkNotificationAs { snoozedUntil: command.snoozedUntil, }); - return mapToDto( - (await this.messageRepository.findOneForInbox({ - _environmentId: command.environmentId, - _id: command.notificationId, - })) as MessageEntity - ); + const updatedMessage = await this.messageRepository.findOneForInbox({ + _environmentId: command.environmentId, + _subscriberId: subscriber._id, + _id: command.notificationId, + contextKeys: command.contextKeys, + }); + if (!updatedMessage) { + throw new NotFoundException(`Notification with id: ${command.notificationId} could not be found after update.`); + } + + return mapToDto(updatedMessage); } } diff --git a/apps/api/src/app/widgets/widgets.controller.ts b/apps/api/src/app/widgets/widgets.controller.ts index 1a4c55a7187..bd2c1d39746 100644 --- a/apps/api/src/app/widgets/widgets.controller.ts +++ b/apps/api/src/app/widgets/widgets.controller.ts @@ -232,6 +232,11 @@ export class WidgetsController { const messageIds = this.toArray(body.messageId); if (!messageIds) throw new BadRequestException('messageId is required'); + const invalidIds = messageIds.filter((id) => !BaseRepository.isInternalId(id)); + if (invalidIds.length > 0) { + throw new BadRequestException(`Invalid messageId format: ${invalidIds.join(', ')}`); + } + return await this.markMessageAsUsecase.execute( MarkMessageAsCommand.create({ organizationId: subscriberSession._organizationId, From 34433b76e275a9e052a088606ba7390d03a25359 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 16 Mar 2026 14:31:59 +0100 Subject: [PATCH 7/7] fix(novu,dashboard): step resolver generated defaults; stale preview fixes NV-7236 (#10319) --- .../steps/controls/custom-step-controls.tsx | 10 +- .../steps/controls/text-widget.tsx | 4 +- apps/dashboard/src/hooks/use-form-autosave.ts | 10 +- .../src/pages/edit-step-template-v2.tsx | 11 +- .../__snapshots__/step-file.spec.ts.snap | 213 +++++++++++++----- .../commands/step/templates/step-file.spec.ts | 6 +- .../src/commands/step/templates/step-file.ts | 141 +++++++++--- 7 files changed, 296 insertions(+), 99 deletions(-) diff --git a/apps/dashboard/src/components/workflow-editor/steps/controls/custom-step-controls.tsx b/apps/dashboard/src/components/workflow-editor/steps/controls/custom-step-controls.tsx index 4e6baec23d6..d77254a7460 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/controls/custom-step-controls.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/controls/custom-step-controls.tsx @@ -34,7 +34,7 @@ export const CustomStepControls = (props: CustomStepControlsProps) => { const [isRestoreDefaultModalOpen, setIsRestoreDefaultModalOpen] = useState(false); const { step, workflow, update } = useWorkflow(); const { saveForm } = useSaveForm(); - const { control } = useFormContext(); + const { control, reset } = useFormContext(); const watchedValues = useWatch({ control }); const dataSchemaDefaults = buildDefaultValuesOfDataSchema(step?.controls.dataSchema ?? {}); @@ -117,6 +117,7 @@ export const CustomStepControls = (props: CustomStepControlsProps) => { if (!workflow || !step) return; update(updateStepInWorkflow(workflow, step.stepId, { controlValues: null })); + reset(dataSchemaDefaults, { keepErrors: true }); setIsRestoreDefaultModalOpen(false); setIsOverridden(false); }} @@ -171,7 +172,12 @@ export const CustomStepControls = (props: CustomStepControlsProps) => { !isOverridden && 'opacity-60 pointer-events-none' )} > - +
diff --git a/apps/dashboard/src/components/workflow-editor/steps/controls/text-widget.tsx b/apps/dashboard/src/components/workflow-editor/steps/controls/text-widget.tsx index 66faf17dff9..18c81eaf8a7 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/controls/text-widget.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/controls/text-widget.tsx @@ -26,7 +26,9 @@ export function TextWidget(props: WidgetProps) { render={({ field, fieldState }) => { let stringValue = ''; - if (typeof field.value === 'string') { + if (disabled) { + stringValue = typeof rjsfValue === 'string' ? rjsfValue : ''; + } else if (typeof field.value === 'string') { stringValue = field.value; } else if (typeof rjsfValue === 'string') { stringValue = rjsfValue; diff --git a/apps/dashboard/src/hooks/use-form-autosave.ts b/apps/dashboard/src/hooks/use-form-autosave.ts index 332ecdb5b47..357619f24e8 100644 --- a/apps/dashboard/src/hooks/use-form-autosave.ts +++ b/apps/dashboard/src/hooks/use-form-autosave.ts @@ -56,9 +56,15 @@ export function useFormAutosave, T extends Fie } const values = { ...previousData, ...data }; - // form.reset(values, { keepErrors: true }); lastSavedDataRef.current = serializedData; - save(values, { onSuccess: options?.onSuccess }); + save(values, { + onSuccess: () => { + // Reset dirty state after successful save so that polling hooks (e.g. useStepResolverPolling) + // are not permanently blocked. keepValues: true avoids regenerating useFieldArray field IDs. + formRef.current.reset(values, { keepErrors: true, keepValues: true }); + options?.onSuccess?.(); + }, + }); }, [formRef, savePropsRef] ); diff --git a/apps/dashboard/src/pages/edit-step-template-v2.tsx b/apps/dashboard/src/pages/edit-step-template-v2.tsx index d75fd9675ba..7c2121b37b9 100644 --- a/apps/dashboard/src/pages/edit-step-template-v2.tsx +++ b/apps/dashboard/src/pages/edit-step-template-v2.tsx @@ -24,10 +24,15 @@ export function EditStepTemplateV2Page() { // on every render where `values` has a new reference — which regenerates all // useFieldArray field IDs and causes visible row flicker on every save round-trip. const hasInitializedRef = useRef(false); + const prevHashRef = useRef(step?.stepResolverHash); useEffect(() => { - if (hasInitializedRef.current || !step) return; - hasInitializedRef.current = true; - form.reset(getControlsDefaultValues(step), { keepErrors: true }); + if (!step) return; + const hashChanged = step.stepResolverHash !== prevHashRef.current; + prevHashRef.current = step.stepResolverHash; + if (!hasInitializedRef.current || hashChanged) { + hasInitializedRef.current = true; + form.reset(getControlsDefaultValues(step), { keepErrors: true }); + } }, [form, step]); const { onBlur, saveForm, saveFormDebounced } = useFormAutosave({ diff --git a/packages/novu/src/commands/step/templates/__snapshots__/step-file.spec.ts.snap b/packages/novu/src/commands/step/templates/__snapshots__/step-file.spec.ts.snap index f520c3de6f6..093a912ba70 100644 --- a/packages/novu/src/commands/step/templates/__snapshots__/step-file.spec.ts.snap +++ b/packages/novu/src/commands/step/templates/__snapshots__/step-file.spec.ts.snap @@ -2,108 +2,203 @@ exports[`generateChatStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; - -export default step.chat('send-chat', async (controls, { payload, subscriber, context, steps }) => ({ - body: \`Hello \${subscriber.firstName ?? 'there'}, a message for you.\`, -})); +import { z } from 'zod'; + +export default step.chat( + 'send-chat', + async (controls, { payload, subscriber }) => ({ + body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`, + }), + { + controlSchema: z.object({ + message: z.string().default('You have a new message.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.chat, + } +); " `; exports[`generateEmailStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; - -export default step.email('plain-email', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: \`

Hello \${subscriber.firstName ?? 'there'},

Your message here.

\`, -})); +import { z } from 'zod'; + +export default step.email( + 'plain-email', + async (controls, { payload, subscriber }) => ({ + subject: controls.subject, + body: \` + + +

\${controls.heading}

+

Hi \${subscriber.firstName ?? 'there'},

+

\${controls.body}

+

View details

+ + + \`, + // Optionally override the sender for this step: + // from: { email: 'noreply@example.com', name: 'My App' }, + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + heading: z.string().default('New activity'), + body: z.string().default('You have a new message.'), + ctaUrl: z.string().default('/'), + }), + // skip: (_controls, { subscriber }) => !subscriber.email, + } +); " `; exports[`generateInAppStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; - -export default step.inApp('in-app-notify', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'New notification', - body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new in-app notification.\`, -})); +import { z } from 'zod'; + +export default step.inApp( + 'in-app-notify', + async (controls, { payload, subscriber }) => ({ + subject: controls.subject, + body: controls.body, + // avatar: subscriber.avatar, + primaryAction: { + label: controls.ctaLabel, + redirect: { url: controls.ctaUrl, target: '_blank' }, + }, + // secondaryAction: { label: 'Dismiss' }, + }), + { + controlSchema: z.object({ + subject: z.string().default('New activity'), + body: z.string().default('You have a new notification.'), + ctaLabel: z.string().default('View details'), + ctaUrl: z.string().default('/'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.in_app, + } +); " `; exports[`generatePushStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; - -export default step.push('send-push', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'New notification', - body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new notification.\`, -})); +import { z } from 'zod'; + +export default step.push( + 'send-push', + async (controls, { payload, subscriber }) => ({ + subject: controls.title, + body: controls.body, + }), + { + controlSchema: z.object({ + title: z.string().default('New activity'), + body: z.string().default('You have a new notification.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.push, + } +); " `; exports[`generateReactEmailStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; import { render } from '@react-email/components'; +import { z } from 'zod'; import EmailTemplate from '../emails/welcome'; -export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: await render( - - ), -})); +export default step.email( + 'welcome-email', + async (controls, { payload, subscriber, steps }) => ({ + subject: controls.subject, + body: await render( + + ), + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + }), + } +); " `; exports[`generateReactEmailStepFile > should match snapshot with different import paths > nested-import 1`] = ` "import { step } from '@novu/framework/step-resolver'; import { render } from '@react-email/components'; +import { z } from 'zod'; import EmailTemplate from '../../src/emails/welcome'; -export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: await render( - - ), -})); +export default step.email( + 'welcome-email', + async (controls, { payload, subscriber, steps }) => ({ + subject: controls.subject, + body: await render( + + ), + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + }), + } +); " `; exports[`generateReactEmailStepFile > should match snapshot with different import paths > relative-import 1`] = ` "import { step } from '@novu/framework/step-resolver'; import { render } from '@react-email/components'; +import { z } from 'zod'; import EmailTemplate from './emails/welcome'; -export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: await render( - - ), -})); +export default step.email( + 'welcome-email', + async (controls, { payload, subscriber, steps }) => ({ + subject: controls.subject, + body: await render( + + ), + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + }), + } +); " `; exports[`generateSmsStepFile > should match snapshot 1`] = ` "import { step } from '@novu/framework/step-resolver'; - -export default step.sms('send-sms', async (controls, { payload, subscriber, context, steps }) => ({ - body: \`Hello \${subscriber.firstName ?? 'there'}, your message here.\`, -})); +import { z } from 'zod'; + +export default step.sms( + 'send-sms', + async (controls, { payload, subscriber }) => ({ + body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`, + }), + { + controlSchema: z.object({ + message: z.string().default('You have a new notification. Reply STOP to unsubscribe.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.phone, + } +); " `; diff --git a/packages/novu/src/commands/step/templates/step-file.spec.ts b/packages/novu/src/commands/step/templates/step-file.spec.ts index ba125a7350a..2172134836d 100644 --- a/packages/novu/src/commands/step/templates/step-file.spec.ts +++ b/packages/novu/src/commands/step/templates/step-file.spec.ts @@ -23,7 +23,8 @@ describe('generateReactEmailStepFile', () => { it('imports render from @react-email/components and calls it', () => { const result = generateReactEmailStepFile(stepId, '../emails/welcome'); - expect(result).toContain("step.email('welcome-email'"); + expect(result).toContain('step.email('); + expect(result).toContain("'welcome-email'"); expect(result).toContain("from '@react-email/components'"); expect(result).toContain('await render('); }); @@ -42,7 +43,8 @@ describe('generateEmailStepFile', () => { it('does not use React Email', () => { const result = generateEmailStepFile('plain-email'); - expect(result).toContain("step.email('plain-email'"); + expect(result).toContain('step.email('); + expect(result).toContain("'plain-email'"); expect(result).not.toContain('@react-email'); expect(result).not.toContain('await render('); }); diff --git a/packages/novu/src/commands/step/templates/step-file.ts b/packages/novu/src/commands/step/templates/step-file.ts index c7c0fa6b510..349dab10d34 100644 --- a/packages/novu/src/commands/step/templates/step-file.ts +++ b/packages/novu/src/commands/step/templates/step-file.ts @@ -11,68 +11,149 @@ function escapeString(value: string): string { export function generateReactEmailStepFile(stepId: string, templateImportPath: string): string { return `import { step } from '@novu/framework/step-resolver'; import { render } from '@react-email/components'; +import { z } from 'zod'; import EmailTemplate from '${escapeString(templateImportPath)}'; -export default step.email('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: await render( - - ), -})); +export default step.email( + '${escapeString(stepId)}', + async (controls, { payload, subscriber, steps }) => ({ + subject: controls.subject, + body: await render( + + ), + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + }), + } +); `; } export function generateEmailStepFile(stepId: string): string { return `import { step } from '@novu/framework/step-resolver'; +import { z } from 'zod'; -export default step.email('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'No Subject', - body: \`

Hello \${subscriber.firstName ?? 'there'},

Your message here.

\`, -})); +export default step.email( + '${escapeString(stepId)}', + async (controls, { payload, subscriber }) => ({ + subject: controls.subject, + body: \` + + +

\${controls.heading}

+

Hi \${subscriber.firstName ?? 'there'},

+

\${controls.body}

+

View details

+ + + \`, + // Optionally override the sender for this step: + // from: { email: 'noreply@example.com', name: 'My App' }, + }), + { + controlSchema: z.object({ + subject: z.string().default('You have a new notification'), + heading: z.string().default('New activity'), + body: z.string().default('You have a new message.'), + ctaUrl: z.string().default('/'), + }), + // skip: (_controls, { subscriber }) => !subscriber.email, + } +); `; } export function generateSmsStepFile(stepId: string): string { return `import { step } from '@novu/framework/step-resolver'; +import { z } from 'zod'; -export default step.sms('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - body: \`Hello \${subscriber.firstName ?? 'there'}, your message here.\`, -})); +export default step.sms( + '${escapeString(stepId)}', + async (controls, { payload, subscriber }) => ({ + body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`, + }), + { + controlSchema: z.object({ + message: z.string().default('You have a new notification. Reply STOP to unsubscribe.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.phone, + } +); `; } export function generatePushStepFile(stepId: string): string { return `import { step } from '@novu/framework/step-resolver'; +import { z } from 'zod'; -export default step.push('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'New notification', - body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new notification.\`, -})); +export default step.push( + '${escapeString(stepId)}', + async (controls, { payload, subscriber }) => ({ + subject: controls.title, + body: controls.body, + }), + { + controlSchema: z.object({ + title: z.string().default('New activity'), + body: z.string().default('You have a new notification.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.push, + } +); `; } export function generateChatStepFile(stepId: string): string { return `import { step } from '@novu/framework/step-resolver'; +import { z } from 'zod'; -export default step.chat('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - body: \`Hello \${subscriber.firstName ?? 'there'}, a message for you.\`, -})); +export default step.chat( + '${escapeString(stepId)}', + async (controls, { payload, subscriber }) => ({ + body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`, + }), + { + controlSchema: z.object({ + message: z.string().default('You have a new message.'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.chat, + } +); `; } export function generateInAppStepFile(stepId: string): string { return `import { step } from '@novu/framework/step-resolver'; +import { z } from 'zod'; -export default step.inApp('${escapeString(stepId)}', async (controls, { payload, subscriber, context, steps }) => ({ - subject: 'New notification', - body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new in-app notification.\`, -})); +export default step.inApp( + '${escapeString(stepId)}', + async (controls, { payload, subscriber }) => ({ + subject: controls.subject, + body: controls.body, + // avatar: subscriber.avatar, + primaryAction: { + label: controls.ctaLabel, + redirect: { url: controls.ctaUrl, target: '_blank' }, + }, + // secondaryAction: { label: 'Dismiss' }, + }), + { + controlSchema: z.object({ + subject: z.string().default('New activity'), + body: z.string().default('You have a new notification.'), + ctaLabel: z.string().default('View details'), + ctaUrl: z.string().default('/'), + }), + // skip: (_controls, { subscriber }) => !subscriber.channels?.in_app, + } +); `; }