From ce59e93db141135cb82b9effb07efa2cfd03d304 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Sun, 15 Feb 2026 15:06:56 +0200 Subject: [PATCH 1/3] fix(dashboard): Allow null controlValues and update restore behavior fixrs NV-7112 fixes NV-7112 (#10037) --- .../src/components/workflow-editor/step-utils.ts | 4 +++- .../steps/controls/custom-step-controls.tsx | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/src/components/workflow-editor/step-utils.ts b/apps/dashboard/src/components/workflow-editor/step-utils.ts index 96b8abf5082..c7347bffe0f 100644 --- a/apps/dashboard/src/components/workflow-editor/step-utils.ts +++ b/apps/dashboard/src/components/workflow-editor/step-utils.ts @@ -105,7 +105,9 @@ export const updateStepInWorkflow = ( steps: workflow.steps.map((step) => { if (step.stepId === stepId) { const existingControlValues = step.controls?.values || {}; - const updatedControlValues = updateStep.controlValues || existingControlValues; + const updatedControlValues = updateStep.controlValues !== undefined + ? updateStep.controlValues + : existingControlValues; return { ...step, 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 c2dc74918c4..37935fc423b 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 @@ -11,6 +11,7 @@ import { InlineToast } from '@/components/primitives/inline-toast'; import { Separator } from '@/components/primitives/separator'; import { Switch } from '@/components/primitives/switch'; import { SidebarContent } from '@/components/side-navigation/sidebar'; +import { updateStepInWorkflow } from '@/components/workflow-editor/step-utils'; import { useSaveForm } from '@/components/workflow-editor/steps/save-form-context'; import { ResourceOriginEnum } from '@/utils/enums'; import { buildDefaultValuesOfDataSchema } from '@/utils/schema'; @@ -29,7 +30,7 @@ const CONTROLS_DOCS_LINK = 'https://docs.novu.co/framework/controls'; export const CustomStepControls = (props: CustomStepControlsProps) => { const { className, dataSchema, origin } = props; const [isRestoreDefaultModalOpen, setIsRestoreDefaultModalOpen] = useState(false); - const { step } = useWorkflow(); + const { step, workflow, update } = useWorkflow(); const [isOverridden, setIsOverridden] = useState(() => Object.keys(step?.controls.values ?? {}).length > 0); const { reset } = useFormContext(); const { saveForm } = useSaveForm(); @@ -98,9 +99,11 @@ export const CustomStepControls = (props: CustomStepControlsProps) => { open={isRestoreDefaultModalOpen} onOpenChange={setIsRestoreDefaultModalOpen} onConfirm={async () => { - const defaultValues = buildDefaultValuesOfDataSchema(step?.controls.dataSchema ?? {}); + if (!workflow || !step) return; + + const defaultValues = buildDefaultValuesOfDataSchema(step.controls.dataSchema ?? {}); reset(defaultValues); - saveForm({ forceSubmit: true }); + update(updateStepInWorkflow(workflow, step.stepId, { controlValues: null })); setIsRestoreDefaultModalOpen(false); setIsOverridden(false); }} @@ -149,7 +152,12 @@ export const CustomStepControls = (props: CustomStepControlsProps) => { -
+
From bb45ee6e1b62523b9b03ec5cacb8ad8845c713b0 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Sun, 15 Feb 2026 15:07:34 +0200 Subject: [PATCH 2/3] style(dashboard): Workflow run terminology fixes NV-7114 (#10035) Co-authored-by: Cursor Agent --- .../src/components/http-logs/workflow-run-activity-drawer.tsx | 2 +- .../test-workflow/test-workflow-activity-drawer.tsx | 2 +- .../test-workflow/test-workflow-logs-sidebar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/src/components/http-logs/workflow-run-activity-drawer.tsx b/apps/dashboard/src/components/http-logs/workflow-run-activity-drawer.tsx index 9aea2006786..555f64a3808 100644 --- a/apps/dashboard/src/components/http-logs/workflow-run-activity-drawer.tsx +++ b/apps/dashboard/src/components/http-logs/workflow-run-activity-drawer.tsx @@ -28,7 +28,7 @@ export const WorkflowRunActivityDrawer = forwardRef - Event Logs + Workflow run
{currentActivityId ? ( diff --git a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-activity-drawer.tsx b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-activity-drawer.tsx index f4236a6de9b..40dc936ef70 100644 --- a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-activity-drawer.tsx +++ b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-activity-drawer.tsx @@ -76,7 +76,7 @@ export const TestWorkflowActivityDrawer = forwardRef - Event Logs + Workflow run
{localTransactionId ? ( diff --git a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-logs-sidebar.tsx b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-logs-sidebar.tsx index 7cd729f6a49..bf418a22faa 100644 --- a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-logs-sidebar.tsx +++ b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-logs-sidebar.tsx @@ -116,7 +116,7 @@ export const TestWorkflowLogsSidebar = (props: TestWorkflowLogsSidebarProps) =>

- No logs to show, trigger test run to see event logs appear here + No logs to show, trigger test run to see workflow run appear here

From 3248d0eabc76b716d645721884db1821333f5c36 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Sun, 15 Feb 2026 15:24:56 +0200 Subject: [PATCH 3/3] feat(worker): Cache workflow preferences in LRU store (#10038) --- .../in-memory-lru-cache.store.ts | 10 +- .../get-preferences.usecase.ts | 115 +++++++++++------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/libs/application-generic/src/services/in-memory-lru-cache/in-memory-lru-cache.store.ts b/libs/application-generic/src/services/in-memory-lru-cache/in-memory-lru-cache.store.ts index 68159a8b66f..91d4fb644e5 100644 --- a/libs/application-generic/src/services/in-memory-lru-cache/in-memory-lru-cache.store.ts +++ b/libs/application-generic/src/services/in-memory-lru-cache/in-memory-lru-cache.store.ts @@ -1,4 +1,4 @@ -import type { EnvironmentEntity, NotificationTemplateEntity, OrganizationEntity } from '@novu/dal'; +import type { EnvironmentEntity, NotificationTemplateEntity, OrganizationEntity, PreferencesEntity } from '@novu/dal'; import type { UserSessionData } from '@novu/shared'; export enum InMemoryLRUCacheStore { @@ -8,6 +8,7 @@ export enum InMemoryLRUCacheStore { API_KEY_USER = 'api-key-user', VALIDATOR = 'validator', ACTIVE_WORKFLOWS = 'active-workflows', + WORKFLOW_PREFERENCES = 'workflow-preferences', } export type WorkflowCacheData = NotificationTemplateEntity | null; @@ -16,6 +17,7 @@ export type EnvironmentCacheData = Pick = { ttl: 1000 * 60, featureFlagComponent: 'active-workflows', }, + [InMemoryLRUCacheStore.WORKFLOW_PREFERENCES]: { + max: 1000, + ttl: 1000 * 60, + featureFlagComponent: 'workflow-preferences', + }, }; 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 5caa86fbcdb..47770a9ec10 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 @@ -11,6 +11,7 @@ import { } from '@novu/shared'; import { Instrument, InstrumentUsecase } from '../../instrumentation'; import { FeatureFlagsService } from '../../services/feature-flags'; +import { InMemoryLRUCacheService, InMemoryLRUCacheStore } from '../../services/in-memory-lru-cache'; import { MergePreferencesCommand } from '../merge-preferences/merge-preferences.command'; import { MergePreferences } from '../merge-preferences/merge-preferences.usecase'; import { GetPreferencesCommand } from './get-preferences.command'; @@ -41,7 +42,8 @@ class PreferencesNotFoundException extends BadRequestException { export class GetPreferences { constructor( private preferencesRepository: PreferencesRepository, - private featureFlagsService: FeatureFlagsService + private featureFlagsService: FeatureFlagsService, + private inMemoryLRUCacheService: InMemoryLRUCacheService ) {} @InstrumentUsecase() @@ -249,12 +251,38 @@ export class GetPreferences { const queryOptions = { readPreference: 'secondaryPreferred' as const }; - const orConditions: Array> = [ - { - _templateId: command.templateId, - type: { $in: [PreferencesTypeEnum.WORKFLOW_RESOURCE, PreferencesTypeEnum.USER_WORKFLOW] }, + const cacheOptions = { + environmentId: command.environmentId, + organizationId: command.organizationId, + }; + + const workflowPreferences = await this.inMemoryLRUCacheService.get( + InMemoryLRUCacheStore.WORKFLOW_PREFERENCES, + `${command.environmentId}:${command.templateId}`, + async (): Promise<[PreferencesEntity | null, PreferencesEntity | null]> => { + const preferences = await this.preferencesRepository.find( + { + ...baseQuery, + _templateId: command.templateId, + type: { $in: [PreferencesTypeEnum.WORKFLOW_RESOURCE, PreferencesTypeEnum.USER_WORKFLOW] }, + }, + undefined, + queryOptions + ); + + const workflowResourcePreference = + preferences.find((p) => p.type === PreferencesTypeEnum.WORKFLOW_RESOURCE) ?? null; + const workflowUserPreference = preferences.find((p) => p.type === PreferencesTypeEnum.USER_WORKFLOW) ?? null; + + return [workflowResourcePreference, workflowUserPreference]; }, - ]; + cacheOptions + ); + + const [workflowResourcePreference, workflowUserPreference] = workflowPreferences; + + let subscriberWorkflowPreference: PreferencesEntity | null = null; + let subscriberGlobalPreference: PreferencesEntity | null = null; if (command.subscriberId) { const useContextFiltering = await this.featureFlagsService.getFlag({ @@ -267,47 +295,48 @@ export class GetPreferences { enabled: useContextFiltering, }); - orConditions.push( - { - _subscriberId: command.subscriberId, - _templateId: command.templateId, - type: PreferencesTypeEnum.SUBSCRIBER_WORKFLOW, - ...contextQuery, - }, - { - _subscriberId: command.subscriberId, - type: PreferencesTypeEnum.SUBSCRIBER_GLOBAL, - ...contextQuery, - } - ); + [subscriberWorkflowPreference, subscriberGlobalPreference] = await Promise.all([ + this.preferencesRepository.findOne( + { + ...baseQuery, + _subscriberId: command.subscriberId, + _templateId: command.templateId, + type: PreferencesTypeEnum.SUBSCRIBER_WORKFLOW, + ...contextQuery, + }, + undefined, + queryOptions + ), + this.preferencesRepository.findOne( + { + ...baseQuery, + _subscriberId: command.subscriberId, + type: PreferencesTypeEnum.SUBSCRIBER_GLOBAL, + ...contextQuery, + }, + undefined, + queryOptions + ), + ]); } - const allPreferences = await this.preferencesRepository.find( - { - ...baseQuery, - $or: orConditions, - }, - undefined, - queryOptions - ); - const result: PreferenceSet = {}; - for (const preference of allPreferences) { - switch (preference.type) { - case PreferencesTypeEnum.WORKFLOW_RESOURCE: - result.workflowResourcePreference = preference as PreferenceSet['workflowResourcePreference']; - break; - case PreferencesTypeEnum.USER_WORKFLOW: - result.workflowUserPreference = preference as PreferenceSet['workflowUserPreference']; - break; - case PreferencesTypeEnum.SUBSCRIBER_WORKFLOW: - result.subscriberWorkflowPreference = preference as PreferenceSet['subscriberWorkflowPreference']; - break; - case PreferencesTypeEnum.SUBSCRIBER_GLOBAL: - result.subscriberGlobalPreference = preference as PreferenceSet['subscriberGlobalPreference']; - break; - } + if (workflowResourcePreference) { + result.workflowResourcePreference = workflowResourcePreference as PreferenceSet['workflowResourcePreference']; + } + + if (workflowUserPreference) { + result.workflowUserPreference = workflowUserPreference as PreferenceSet['workflowUserPreference']; + } + + if (subscriberWorkflowPreference) { + result.subscriberWorkflowPreference = + subscriberWorkflowPreference as PreferenceSet['subscriberWorkflowPreference']; + } + + if (subscriberGlobalPreference) { + result.subscriberGlobalPreference = subscriberGlobalPreference as PreferenceSet['subscriberGlobalPreference']; } return result;