From bdf06fa8ddb9b07c937531b3af22aac5882e1a5f Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Fri, 1 May 2026 12:04:55 +0200 Subject: [PATCH] refactor(editor): Migrate whole workflow object consumers (#29395) --- .../app/components/MainHeader/MainHeader.vue | 7 +- .../components/MainHeader/WorkflowDetails.vue | 2 +- .../WorkflowProductionChecklist.test.ts | 338 ++++-------------- .../WorkflowProductionChecklist.vue | 43 +-- .../components/WorkflowShareModal.ee.test.ts | 49 ++- .../app/components/WorkflowShareModal.ee.vue | 6 +- .../composables/useCanvasOperations.test.ts | 2 +- .../app/composables/useCanvasOperations.ts | 30 +- .../src/app/composables/useDataSchema.test.ts | 6 + .../src/app/composables/useDataSchema.ts | 12 +- .../src/app/composables/useNodeHelpers.ts | 2 +- .../src/app/composables/usePinnedData.ts | 3 +- .../handlers/executionFinished.ts | 6 +- .../handlers/executionStarted.ts | 21 +- .../app/composables/useRunWorkflow.test.ts | 39 +- .../src/app/composables/useRunWorkflow.ts | 4 +- .../app/composables/useWorkflowExtraction.ts | 4 +- .../src/app/composables/useWorkflowHelpers.ts | 2 +- .../src/app/composables/useWorkflowState.ts | 4 +- .../src/app/stores/workflowDocument.store.ts | 39 +- .../src/app/stores/workflows.store.ts | 3 + .../editor-ui/src/app/views/NodeView.vue | 32 +- .../ai/assistant/assistant.store.test.ts | 1 + .../features/ai/assistant/assistant.store.ts | 9 +- .../features/ai/assistant/builder.store.ts | 2 +- .../composables/useBuilderExecution.ts | 2 +- .../composables/useReviewChanges.test.ts | 1 + .../assistant/composables/useReviewChanges.ts | 4 +- .../src/features/ai/chatHub/chat.store.ts | 5 +- .../components/InstanceAiWorkflowSetup.vue | 2 +- .../views/WorkflowExecutionsView.vue | 12 +- .../logs/components/LogsOverviewPanel.test.ts | 11 +- .../useClearExecutionButtonVisible.ts | 11 +- .../views/SettingsSourceControl.test.ts | 8 +- .../panel/components/NDVSubConnections.vue | 4 +- .../components/ExpressionEditModal.vue | 2 +- .../runData/components/VirtualSchema.test.ts | 2 + .../ndv/runData/components/VirtualSchema.vue | 9 +- .../ndv/shared/views/NodeDetailsView.test.ts | 44 +-- .../ndv/shared/views/NodeDetailsView.vue | 27 +- .../shared/views/NodeDetailsViewV2.test.ts | 29 +- .../ndv/shared/views/NodeDetailsViewV2.vue | 29 +- .../composables/useNodeCommands.test.ts | 48 ++- .../commandBar/composables/useNodeCommands.ts | 6 +- .../composables/useWorkflowCommands.test.ts | 25 +- .../composables/useWorkflowCommands.ts | 14 +- .../plugins/codemirror/completions/utils.ts | 10 +- .../canvas/components/WorkflowCanvas.test.ts | 53 ++- .../canvas/components/WorkflowCanvas.vue | 26 +- .../composables/useExpressionResolveCtx.ts | 2 +- 50 files changed, 476 insertions(+), 576 deletions(-) diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue b/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue index 1ce911fbe52..cc5235cb2f1 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue @@ -14,7 +14,6 @@ import { useExecutionsStore } from '@/features/execution/executions/executions.s import { useNDVStore } from '@/features/ndv/shared/ndv.store'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useUIStore } from '@/app/stores/ui.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { computed, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import type { RouteLocation, RouteLocationRaw } from 'vue-router'; @@ -35,7 +34,6 @@ const pushConnection = usePushConnection({ router }); const toast = useToast(); const ndvStore = useNDVStore(); const uiStore = useUIStore(); -const workflowsStore = useWorkflowsStore(); const workflowsListStore = useWorkflowsListStore(); const executionsStore = useExecutionsStore(); const settingsStore = useSettingsStore(); @@ -71,7 +69,6 @@ const activeNode = computed(() => ndvStore.activeNode); const hideMenuBar = computed(() => Boolean(activeNode.value && activeNode.value.type !== STICKY_NODE_TYPE), ); -const workflow = computed(() => workflowsStore.workflow); const workflowId = useInjectWorkflowId(); const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null); const workflowName = computed(() => workflowDocumentStore?.value?.name ?? ''); @@ -265,7 +262,7 @@ async function onWorkflowDeactivated() { ) { try { // Fetch the updated workflow to get the latest settings after backend processing - const updatedWorkflow = await workflowsListStore.fetchWorkflow(workflow.value.id); + const updatedWorkflow = await workflowsListStore.fetchWorkflow(workflowId.value); workflowDocumentStore?.value?.hydrate(updatedWorkflow); toast.showToast({ title: locale.baseText('mcp.workflowDeactivated.title'), @@ -291,7 +288,7 @@ async function onWorkflowDeactivated() {
{ - + { createWorkflowDocumentId(mockWorkflow.id), ); workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); + workflowDocumentStore.setSettings(mockWorkflow.settings ?? { executionOrder: 'v1' }); workflowDocumentStoreRef.value = workflowDocumentStore; router = { @@ -203,16 +204,7 @@ describe('WorkflowProductionChecklist', () => { activeVersion: null, }); - const { container } = renderComponent({ - props: { - workflow: { - ...mockWorkflow, - active: false, - activeVersionId: null, - }, - }, - pinia: createTestingPinia(), - }); + const { container } = renderComponent({ pinia: createTestingPinia() }); expect( container.querySelector('[data-test-id="n8n-suggested-actions-stub"]'), @@ -222,12 +214,7 @@ describe('WorkflowProductionChecklist', () => { it('should not render when cache is loading', () => { workflowsCache.isCacheLoading.value = true; - const { container } = renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + const { container } = renderComponent({ pinia: createTestingPinia() }); expect( container.querySelector('[data-test-id="n8n-suggested-actions-stub"]'), @@ -244,15 +231,11 @@ describe('WorkflowProductionChecklist', () => { // @ts-expect-error - mocking readonly property nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })], - }, - }, - pinia, - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: 'ai-node', typeVersion: 1 }), + ]); + + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -292,15 +275,11 @@ describe('WorkflowProductionChecklist', () => { .fn() .mockReturnValue(mockNonAINodeType as INodeTypeDescription); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [createTestNode({ type: 'regular-node', typeVersion: 1 })], - }, - }, - pinia, - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: 'regular-node', typeVersion: 1 }), + ]); + + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -323,12 +302,7 @@ describe('WorkflowProductionChecklist', () => { }); it('should show error workflow action and time saved when not ignored', async () => { - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -359,12 +333,7 @@ describe('WorkflowProductionChecklist', () => { }, }); - const { container } = renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + const { container } = renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect( @@ -374,15 +343,11 @@ describe('WorkflowProductionChecklist', () => { }); it('should not show error workflow action when workflow contains an enabled Error Trigger node', async () => { - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1 })], - }, - }, - pinia: createTestingPinia(), - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1 }), + ]); + + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -398,17 +363,11 @@ describe('WorkflowProductionChecklist', () => { }); it('should show error workflow action when workflow contains a disabled Error Trigger node', async () => { - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [ - createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1, disabled: true }), - ], - }, - }, - pinia: createTestingPinia(), - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1, disabled: true }), + ]); + + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -441,15 +400,11 @@ describe('WorkflowProductionChecklist', () => { // @ts-expect-error - mocking readonly property nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })], - }, - }, - pinia, - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: 'ai-node', typeVersion: 1 }), + ]); + + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -470,12 +425,7 @@ describe('WorkflowProductionChecklist', () => { uiStore = useUIStore(pinia); const openModalSpy = vi.spyOn(uiStore, 'openModal'); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -493,12 +443,7 @@ describe('WorkflowProductionChecklist', () => { uiStore = useUIStore(pinia); const openModalSpy = vi.spyOn(uiStore, 'openModal'); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -512,12 +457,7 @@ describe('WorkflowProductionChecklist', () => { }); it('should ignore specific action when ignore is clicked', async () => { - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -537,12 +477,7 @@ describe('WorkflowProductionChecklist', () => { }); it('should ignore all actions after confirmation', async () => { - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -568,12 +503,7 @@ describe('WorkflowProductionChecklist', () => { it('should not ignore all actions if confirmation is cancelled', async () => { message.confirm = vi.fn().mockResolvedValue('cancel'); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -593,12 +523,7 @@ describe('WorkflowProductionChecklist', () => { describe('Popover behavior', () => { it('should track when popover is opened via update:open event', async () => { - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -622,16 +547,7 @@ describe('WorkflowProductionChecklist', () => { activeVersion: null, }); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - active: false, - activeVersionId: null, - }, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); workflowDocumentStoreRef.value?.setActiveState({ activeVersionId: 'v1', @@ -658,16 +574,7 @@ describe('WorkflowProductionChecklist', () => { activeVersion: null, }); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - active: false, - activeVersionId: null, - }, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await flushPromises(); @@ -703,16 +610,7 @@ describe('WorkflowProductionChecklist', () => { activeVersion: null, }); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - active: false, - activeVersionId: null, - }, - }, - pinia, - }); + renderComponent({ pinia }); await flushPromises(); @@ -740,12 +638,7 @@ describe('WorkflowProductionChecklist', () => { writable: true, }); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -768,15 +661,11 @@ describe('WorkflowProductionChecklist', () => { // @ts-expect-error - mocking readonly property nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })], - }, - }, - pinia, - }); + workflowDocumentStoreRef.value?.setNodes([ + createTestNode({ type: 'ai-node', typeVersion: 1 }), + ]); + + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -806,19 +695,13 @@ describe('WorkflowProductionChecklist', () => { }); it('should mark error workflow as completed when error workflow is set', async () => { - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - settings: { - executionOrder: 'v1', - errorWorkflow: 'error-workflow-id', - }, - }, - }, - pinia: createTestingPinia(), + workflowDocumentStoreRef.value?.setSettings({ + executionOrder: 'v1', + errorWorkflow: 'error-workflow-id', }); + renderComponent({ pinia: createTestingPinia() }); + await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ { @@ -840,19 +723,13 @@ describe('WorkflowProductionChecklist', () => { }); it('should mark time saved as completed when time saved is set', async () => { - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - settings: { - executionOrder: 'v1', - timeSavedPerExecution: 10, - }, - }, - }, - pinia: createTestingPinia(), + workflowDocumentStoreRef.value?.setSettings({ + executionOrder: 'v1', + timeSavedPerExecution: 10, }); + renderComponent({ pinia: createTestingPinia() }); + await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ { @@ -885,17 +762,7 @@ describe('WorkflowProductionChecklist', () => { }); workflowDocumentStoreRef.value = workflowDocumentStore; - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - settings: { - executionOrder: 'v1', - }, - }, - }, - pinia: createTestingPinia(), - }); + renderComponent({ pinia: createTestingPinia() }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -924,12 +791,7 @@ describe('WorkflowProductionChecklist', () => { settingsStore = useSettingsStore(pinia); vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(false); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toEqual([ @@ -962,12 +824,7 @@ describe('WorkflowProductionChecklist', () => { }); vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(true); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toContainEqual({ @@ -992,12 +849,7 @@ describe('WorkflowProductionChecklist', () => { vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(false); vi.spyOn(usersStore, 'isInstanceOwner', 'get').mockReturnValue(false); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { const actions = mockN8nSuggestedActionsProps.actions; @@ -1023,12 +875,7 @@ describe('WorkflowProductionChecklist', () => { }, }); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { const actions = mockN8nSuggestedActionsProps.actions; @@ -1046,12 +893,7 @@ describe('WorkflowProductionChecklist', () => { mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false }, }); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toContainEqual({ @@ -1073,19 +915,13 @@ describe('WorkflowProductionChecklist', () => { mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false }, }); - renderComponent({ - props: { - workflow: { - ...mockWorkflow, - settings: { - executionOrder: 'v1', - availableInMCP: true, - }, - }, - }, - pinia, + workflowDocumentStoreRef.value?.setSettings({ + executionOrder: 'v1', + availableInMCP: true, }); + renderComponent({ pinia }); + await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toContainEqual({ id: 'workflow-mcp-access', @@ -1112,12 +948,7 @@ describe('WorkflowProductionChecklist', () => { }, }); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { const actions = mockN8nSuggestedActionsProps.actions; @@ -1137,12 +968,7 @@ describe('WorkflowProductionChecklist', () => { }); vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(true); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -1166,12 +992,7 @@ describe('WorkflowProductionChecklist', () => { mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false }, }); - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -1194,12 +1015,7 @@ describe('WorkflowProductionChecklist', () => { branchReadOnly: true, } as SourceControlPreferences; - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -1217,12 +1033,7 @@ describe('WorkflowProductionChecklist', () => { branchReadOnly: false, } as SourceControlPreferences; - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); @@ -1236,12 +1047,7 @@ describe('WorkflowProductionChecklist', () => { sourceControlStore.preferences = {} as SourceControlPreferences; - renderComponent({ - props: { - workflow: mockWorkflow, - }, - pinia, - }); + renderComponent({ pinia }); await vi.waitFor(() => { expect(mockN8nSuggestedActionsProps.actions).toBeDefined(); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue b/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue index 77333ac2604..1499d4e5948 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue @@ -7,7 +7,6 @@ import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import type { ActionType, WorkflowSettings } from '@/app/composables/useWorkflowsCache'; import { useWorkflowSettingsCache } from '@/app/composables/useWorkflowsCache'; import { useUIStore } from '@/app/stores/ui.store'; -import type { IWorkflowDb } from '@/Interface'; import { WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY, @@ -29,10 +28,6 @@ import { useSettingsStore } from '@/app/stores/settings.store'; import { useUsersStore } from '@/features/settings/users/users.store'; import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; -const props = defineProps<{ - workflow: IWorkflowDb; -}>(); - const i18n = useI18n(); const router = useRouter(); const evaluationStore = useEvaluationStore(); @@ -49,9 +44,9 @@ const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null); const isPopoverOpen = ref(false); const cachedSettings = ref(null); +const nodes = computed(() => workflowDocumentStore?.value?.allNodes ?? []); const hasAINode = computed(() => { - const nodes = props.workflow.nodes; - return nodes.some((node) => { + return nodes.value.some((node) => { const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); return nodeType?.codex?.categories?.includes('AI'); }); @@ -62,28 +57,22 @@ const hasEvaluationSetOutputsNode = computed((): boolean => { }); const hasErrorWorkflow = computed(() => { - const errorWorkflow = - workflowDocumentStore?.value?.settings?.errorWorkflow ?? props.workflow.settings?.errorWorkflow; + const errorWorkflow = workflowDocumentStore?.value?.settings?.errorWorkflow; return !!errorWorkflow; }); const isErrorWorkflow = computed(() => { - return props.workflow.nodes.some( + return nodes.value.some( (node) => node.type === ERROR_TRIGGER_NODE_TYPE && node.disabled !== true, ); }); const hasSavedTimeNodes = computed(() => { - if (!props.workflow?.nodes) return false; - return props.workflow.nodes.some( - (node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true, - ); + return nodes.value.some((node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true); }); const hasTimeSaved = computed(() => { - const timeSavedPerExecution = - workflowDocumentStore?.value?.settings?.timeSavedPerExecution ?? - props.workflow.settings?.timeSavedPerExecution; + const timeSavedPerExecution = workflowDocumentStore?.value?.settings?.timeSavedPerExecution; return timeSavedPerExecution !== undefined || hasSavedTimeNodes.value; }); @@ -214,18 +203,17 @@ const availableActions = computed(() => { ...baseAction, id: 'workflow-mcp-access', description: i18n.baseText('mcp.productionChecklist.workflow.description'), - completed: - workflowDocumentStore?.value?.settings?.availableInMCP ?? - props.workflow.settings?.availableInMCP ?? - false, + completed: workflowDocumentStore?.value?.settings?.availableInMCP ?? false, }; } }); async function loadWorkflowSettings() { - if (props.workflow.id) { + if (workflowDocumentStore?.value?.workflowId) { // todo add global config - cachedSettings.value = await workflowsCache.getMergedWorkflowSettings(props.workflow.id); + cachedSettings.value = await workflowsCache.getMergedWorkflowSettings( + workflowDocumentStore?.value.workflowId, + ); } } @@ -235,7 +223,7 @@ async function handleActionClick(actionId: string) { // Navigate to evaluations await router.push({ name: VIEWS.EVALUATION_EDIT, - params: { workflowId: props.workflow.id }, + params: { workflowId: workflowDocumentStore?.value?.workflowId }, }); break; case 'errorWorkflow': @@ -269,7 +257,10 @@ async function handleIgnoreClick(actionId: string) { return; } - await workflowsCache.ignoreSuggestedAction(props.workflow.id, actionId); + await workflowsCache.ignoreSuggestedAction( + workflowDocumentStore?.value?.workflowId ?? '', + actionId, + ); await loadWorkflowSettings(); telemetry.track('user clicked ignore suggested action', { @@ -326,7 +317,7 @@ watch( } // Update firstActivatedAt after opening popover - await workflowsCache.updateFirstActivatedAt(props.workflow.id); + await workflowsCache.updateFirstActivatedAt(workflowDocumentStore?.value?.workflowId ?? ''); } }, ); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.test.ts b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.test.ts index 7fa3cb2472c..f959702ac56 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.test.ts +++ b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.test.ts @@ -17,6 +17,18 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowsEEStore } from '@/app/stores/workflows.ee.store'; import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; import { useRolesStore } from '@/app/stores/roles.store'; +import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types'; + +const mockWorkflowDocumentState = reactive({ + homeProject: null as ProjectSharingData | null, + scopes: [] as string[], + sharedWithProjects: [] as ProjectSharingData[], + name: '', +}); +vi.mock('@/app/stores/workflowDocument.store', () => ({ + useWorkflowDocumentStore: () => mockWorkflowDocumentState, + createWorkflowDocumentId: (id: string) => `${id}@latest`, +})); const mockRouteQuery = reactive>({}); vi.mock('vue-router', async (importOriginal) => { @@ -73,7 +85,6 @@ let workflowsStore: MockedStore; let workflowsEEStore: MockedStore; let projectsStore: MockedStore; let rolesStore: MockedStore; - describe('WorkflowShareModal.ee.vue', () => { beforeEach(() => { settingsStore = mockedStore(useSettingsStore); @@ -84,6 +95,10 @@ describe('WorkflowShareModal.ee.vue', () => { // Reset route query Object.keys(mockRouteQuery).forEach((key) => delete mockRouteQuery[key]); + mockWorkflowDocumentState.homeProject = null; + mockWorkflowDocumentState.sharedWithProjects = []; + mockWorkflowDocumentState.scopes = []; + mockWorkflowDocumentState.name = ''; // Set up default store state settingsStore.settings.enterprise = { sharing: true } as FrontendSettings['enterprise']; @@ -121,6 +136,15 @@ describe('WorkflowShareModal.ee.vue', () => { // Set route query to indicate new workflow mockRouteQuery.new = 'true'; + const homeProject: ProjectSharingData = { + id: 'personal-project-id', + name: 'Personal Project', + type: ProjectTypes.Personal, + icon: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + workflowsStore.workflow = { id: '', name: 'My workflow', @@ -133,8 +157,11 @@ describe('WorkflowShareModal.ee.vue', () => { scopes: [], nodes: [], connections: {}, + homeProject, }; + mockWorkflowDocumentState.homeProject = homeProject; + const saveWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'saveWorkflowSharedWith'); const props = { @@ -174,6 +201,15 @@ describe('WorkflowShareModal.ee.vue', () => { type: ProjectTypes.Personal, }); + const homeProject: ProjectSharingData = { + id: 'personal-project-id', + name: 'Personal Project', + type: ProjectTypes.Personal, + icon: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + workflowsStore.workflow = { id: 'workflow-1', name: 'My workflow', @@ -186,16 +222,11 @@ describe('WorkflowShareModal.ee.vue', () => { scopes: [], nodes: [], connections: {}, - homeProject: { - id: 'personal-project-id', - name: 'Personal Project', - type: ProjectTypes.Personal, - icon: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }, + homeProject, }; + mockWorkflowDocumentState.homeProject = homeProject; + const props = { data: { id: 'workflow-1' }, }; diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue index 3e8e9cd3442..558e23be003 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue @@ -66,10 +66,7 @@ const workflowName = computed( () => workflowListEntry.value?.name ?? workflowDocumentStore.value.name, ); const workflowHomeProject = computed( - () => - workflowListEntry.value?.homeProject ?? - workflowDocumentStore.value.homeProject ?? - workflowsStore.workflow.homeProject, + () => workflowListEntry.value?.homeProject ?? workflowDocumentStore.value.homeProject, ); const workflowScopes = computed( () => workflowListEntry.value?.scopes ?? workflowDocumentStore.value.scopes, @@ -304,6 +301,7 @@ watch(
{ // Tests that need custom behavior can override via vi.spyOn. vi.mocked(workflowDocumentStoreInstance.getParentNodesByDepth).mockReturnValue([]); vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]); - vi.mocked(workflowDocumentStoreInstance.getSnapshot).mockReturnValue({ + vi.mocked(workflowDocumentStoreInstance.getWorkflowObjectAccessorSnapshot).mockReturnValue({ id: workflowDocumentStoreInstance.workflowId, connectionsBySourceNode: workflowDocumentStoreInstance.connectionsBySourceNode, pinData: workflowDocumentStoreInstance.pinData as IPinData, diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index 63b2c4f7f9f..c7d3d6bfe8b 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -211,10 +211,10 @@ export function useCanvasOperations() { const preventOpeningNDV = !!localStorage.getItem('NodeView.preventOpeningNDV'); - const editableWorkflow = computed(() => workflowsStore.workflow); - const editableWorkflowObject = computed(() => - workflowDocumentStore.value ? workflowDocumentStore.value.getSnapshot() : undefined, + workflowDocumentStore.value + ? workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot() + : undefined, ); const triggerNodes = computed(() => { @@ -2708,12 +2708,16 @@ export function useCanvasOperations() { // the user workflowHelpers.updateNodePositions( workflowData, - NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value, { - ...(workflowData.nodes && workflowData.nodes.length > 1 - ? { size: getNodesGroupSize(workflowData.nodes) } - : {}), - viewport, - }), + NodeViewUtils.getNewNodePosition( + workflowDocumentStore.value.allNodes, + lastClickPosition.value, + { + ...(workflowData.nodes && workflowData.nodes.length > 1 + ? { size: getNodesGroupSize(workflowData.nodes) } + : {}), + viewport, + }, + ), ); await addImportedNodesToWorkflow(workflowData, { @@ -3076,10 +3080,11 @@ export function useCanvasOperations() { telemetry: true, }); - const offsetIndex = editableWorkflow.value.nodes.length - nodes.length; + const allNodes = workflowDocumentStore.value.allNodes; + const offsetIndex = allNodes.length - nodes.length; const connections: CanvasConnectionCreateData[] = addedConnections.map(({ from, to }) => { - const fromNode = editableWorkflow.value.nodes[offsetIndex + from.nodeIndex]; - const toNode = editableWorkflow.value.nodes[offsetIndex + to.nodeIndex]; + const fromNode = allNodes[offsetIndex + from.nodeIndex]; + const toNode = allNodes[offsetIndex + to.nodeIndex]; const type = from.type ?? to.type ?? NodeConnectionTypes.Main; return { @@ -3251,7 +3256,6 @@ export function useCanvasOperations() { return { lastClickPosition, - editableWorkflow, editableWorkflowObject, triggerNodes, requireNodeTypeDescription, diff --git a/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts b/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts index 21669ee5357..969e0f0395b 100644 --- a/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts @@ -15,6 +15,12 @@ import type { JSONSchema7 } from 'json-schema'; import { mock } from 'vitest-mock-extended'; vi.mock('@/app/stores/workflows.store'); +vi.mock('@/app/stores/workflowDocument.store', () => ({ + createWorkflowDocumentId: vi.fn(() => 'test'), + useWorkflowDocumentStore: vi.fn(() => ({ + getSettingsSnapshot: () => ({ binaryMode: undefined }), + })), +})); describe('useDataSchema', () => { const getSchema = useDataSchema().getSchema; diff --git a/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts b/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts index 355561a31ca..04fb4e03b88 100644 --- a/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts +++ b/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts @@ -26,9 +26,10 @@ import { type ITaskDataConnections, NodeConnectionTypes, } from 'n8n-workflow'; -import { ref } from 'vue'; +import { computed, ref } from 'vue'; import { type IconName } from '@n8n/design-system/components/N8nIcon/icons'; import { DATA_TYPE_ICON_MAP } from '@/app/constants'; +import { DEFAULT_SETTINGS } from '../stores/workflowDocument/useWorkflowDocumentSettings'; export function useDataSchema() { function getSchema( @@ -554,6 +555,11 @@ export const useFlattenSchema = () => { return acc; } + const workflowsStore = useWorkflowsStore(); + const workflowDocumentStore = computed(() => + useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), + ); + acc = acc.concat( flattenSchema({ isDataEmpty: item.isDataEmpty, @@ -567,7 +573,9 @@ export const useFlattenSchema = () => { expressionPrefix: getNodeParentExpression({ nodeName: item.node.name, distanceFromActive: item.depth, - binaryMode: useWorkflowsStore().workflow.settings?.binaryMode, + binaryMode: + workflowDocumentStore.value.getSettingsSnapshot().binaryMode ?? + DEFAULT_SETTINGS.binaryMode, }), }), ); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts index 3944c49dc21..d441a2aad81 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts @@ -288,7 +288,7 @@ export function useNodeHelpers() { } const nodeInputIssues = getNodeInputIssues( - workflowDocumentStore.value.getSnapshot(), + workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(), node, nodeType, ); diff --git a/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts b/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts index 760195c7882..d5be2a4c914 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts @@ -178,7 +178,8 @@ export function usePinnedData( if (typeof data === 'object') data = JSON.stringify(data); - const { pinData: _pinData, ...workflowObjectWithoutPinData } = workflowsStore.workflow; + const { pinData: _pinData, ...workflowObjectWithoutPinData } = + workflowDocumentStore.value?.getSnapshot() ?? {}; const currentPinData = (workflowDocumentStore.value?.pinData ?? {}) as IPinData; const workflowJson = jsonStringify(workflowObjectWithoutPinData, { replaceCircularRefs: true }); diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts index aef48c66f2b..9cf54e97af6 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts @@ -207,6 +207,10 @@ export async function fetchExecutionData( executionId: string, ): Promise { const workflowsStore = useWorkflowsStore(); + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + try { const executionResponse = await workflowsStore.fetchExecutionDataById(executionId); if (!executionResponse?.data) { @@ -216,7 +220,7 @@ export async function fetchExecutionData( return { id: executionId, workflowId: executionResponse.workflowId, - workflowData: workflowsStore.workflow, + workflowData: workflowDocumentStore.getSnapshot(), data: executionResponse.data, status: executionResponse.status, startedAt: workflowsStore.workflowExecutionData?.startedAt as Date, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts index 0c5933258d5..3d42bb52399 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts @@ -38,10 +38,10 @@ export async function executionStarted( // Initialize or reinitialize workflowExecutionData to clear previous execution's // node status (e.g. DemoLayout iframe receiving push events for a new execution). if (!workflowsStore.workflowExecutionData?.data || needsInit) { - const wf = workflowsStore.workflow; - const workflowDocumentStore = workflowsStore.workflowId - ? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)) - : undefined; + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + options.workflowState.setWorkflowExecutionData({ id: data.executionId, finished: false, @@ -49,18 +49,7 @@ export async function executionStarted( status: 'running', createdAt: new Date(), startedAt: new Date(), - workflowData: { - id: wf.id, - name: workflowDocumentStore?.name ?? '', - active: wf.active, - isArchived: wf.isArchived, - nodes: wf.nodes, - connections: wf.connections, - createdAt: wf.createdAt, - updatedAt: wf.updatedAt, - versionId: wf.versionId ?? '', - activeVersionId: wf.activeVersionId ?? null, - }, + workflowData: workflowDocumentStore.getSnapshot(), data: createRunExecutionData(), }); } diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts index 876282f0d00..f8172153271 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts @@ -20,7 +20,7 @@ import { } from '@/app/composables/useWorkflowState'; import { chatEventBus } from '@n8n/chat/event-buses'; import { useChat } from '@n8n/chat/composables'; -import type { IStartRunData } from '@/Interface'; +import type { INodeUi, IStartRunData } from '@/Interface'; import type { IExecutionResponse } from '@/features/execution/executions/executions.types'; import type { WorkflowData } from '@n8n/rest-api-client/api/workflows'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; @@ -43,39 +43,45 @@ import { CHAT_HITL_TOOL_NODE_TYPE, } from '../constants'; import type { WorkflowObjectAccessors } from '../types'; +import type { useWorkflowDocumentStore } from '../stores/workflowDocument.store'; +import type { Mocked } from 'vitest'; + +type Writable = { -readonly [K in keyof T]: T[K] }; const { mockDocumentStore } = vi.hoisted(() => { const store = { workflowId: '123', name: 'Test Workflow', - allNodes: [] as unknown[], + allNodes: [], getNodeByName: vi.fn(), getParentNodes: vi.fn().mockReturnValue([]), getChildNodes: vi.fn().mockReturnValue([]), getStartNode: vi.fn(), checkIfNodeHasChatParent: vi.fn(), checkIfToolNodeHasChatParent: vi.fn(), - connectionsBySourceNode: {} as Record, - pinData: {} as Record, + connectionsBySourceNode: {}, + pinData: {}, incomingConnectionsByNodeName: vi.fn().mockReturnValue({}), outgoingConnectionsByNodeName: vi.fn().mockReturnValue({}), nodesIssuesExist: false, getParametersLastUpdate: vi.fn(), getPinnedDataLastUpdate: vi.fn(), getPinnedDataLastRemovedAt: vi.fn(), - getSnapshot: vi.fn(), + getWorkflowObjectAccessorSnapshot: vi.fn(), hasNodeValidationIssues: false, nodeValidationIssues: [], serialize: vi.fn(), - }; - store.getSnapshot.mockReturnValue({ + } as Partial>>> as Mocked< + Writable> + >; + store.getWorkflowObjectAccessorSnapshot.mockReturnValue({ id: store.workflowId, getNode: store.getNodeByName, getParentNodes: store.getParentNodes, getChildNodes: store.getChildNodes, connectionsBySourceNode: store.connectionsBySourceNode, - pinData: store.pinData, - }); + pinData: store.pinData as IPinData, + } as Partial as WorkflowObjectAccessors); return { mockDocumentStore: store }; }); @@ -433,7 +439,7 @@ describe('useRunWorkflow({ router })', () => { return []; }); vi.mocked(mockDocumentStore.getNodeByName).mockImplementation((name: string) => { - const nodes: Record = { + const nodes: Record = { [parentNodeName]: createTestNode({ name: parentNodeName }), [destinationNodeName]: createTestNode({ name: destinationNodeName }), }; @@ -568,9 +574,7 @@ describe('useRunWorkflow({ router })', () => { const composable = useRunWorkflow({ router }); const triggerNode = 'Chat Trigger'; const nodeData = mock(); - vi.mocked(mockDocumentStore.getChildNodes).mockReturnValue([ - { name: 'Child node', type: 'nodes.child' }, - ]); + vi.mocked(mockDocumentStore.getChildNodes).mockReturnValue(['Child node']); mockDocumentStore.serialize.mockReturnValue(mock({ nodes: [] })); const { runWorkflow } = composable; @@ -587,10 +591,7 @@ describe('useRunWorkflow({ router })', () => { }, startNodes: [ { - name: { - name: 'Child node', - type: 'nodes.child', - }, + name: 'Child node', sourceData: null, }, ], @@ -670,7 +671,7 @@ describe('useRunWorkflow({ router })', () => { }; vi.mocked(mockDocumentStore.getNodeByName).mockImplementation((name: string) => - name === 'Test node' ? { id: 'Test id', name: 'Test node' } : undefined, + name === 'Test node' ? createTestNode({ id: 'Test id', name: 'Test node' }) : null, ); vi.mocked(pushConnectionStore).isConnected = true; @@ -1280,7 +1281,7 @@ describe('useRunWorkflow({ router })', () => { if (name === topNode) return getNodeUi(topNode, [100, 50]); if (name === middleNode) return getNodeUi(middleNode, [200, 200]); if (name === bottomNode) return getNodeUi(bottomNode, [150, 350]); - return undefined; + return null; }); // Test with different order of input nodes diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts index 36e2066b544..b5ad667229c 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts @@ -189,7 +189,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { directParentNodes, runData, workflowData.pinData, - workflowDocumentStore.value.getSnapshot(), + workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(), ); const { startNodeNames } = consolidatedData; @@ -539,7 +539,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { // execution finished before it could be stopped const executedData = { data: execution.data, - workflowData: workflowsStore.workflow, + workflowData: workflowDocumentStore.value.getSnapshot(), finished: execution.finished, mode: execution.mode, startedAt: execution.startedAt, diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts index ef97bd47e36..e291bac9004 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts @@ -407,7 +407,7 @@ export function useWorkflowExtraction() { ); for (const node of selectionChildNodes) { - const currentNode = workflowsStore.workflow.nodes.find((x) => x.id === node.id); + const currentNode = workflowDocumentStore?.value?.allNodes.find((x) => x.id === node.id); if (isEqual(node, currentNode)) continue; @@ -461,7 +461,7 @@ export function useWorkflowExtraction() { ) { const { start, end } = selection; - const allNodeNames = workflowsStore.workflow.nodes.map((x) => x.name); + const allNodeNames = workflowDocumentStore?.value?.allNodes.map((x) => x.name) ?? []; let startNodeName = 'Start'; const subGraphNames = subGraph.map((x) => x.name); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index e0eccb25525..b2e40e2f3d9 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -98,7 +98,7 @@ export async function resolveParameter( return await resolveParameterImpl( parameter, - workflowDocumentStore.getSnapshot(), + workflowDocumentStore.getWorkflowObjectAccessorSnapshot(), workflowDocumentStore.connectionsBySourceNode, useEnvironmentsStore().variablesAsObject, useNDVStore().activeNode, diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts index ce3c1b0a7ef..040c466d0d8 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts @@ -84,9 +84,7 @@ export function useWorkflowState() { setActiveExecutionId(undefined); workflowStateStore.executingNode.clearNodeExecutionQueue(); ws.executionWaitingForWebhook = false; - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(ws.workflow.id), - ); + const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(ws.workflowId)); documentTitle.setDocumentTitle(workflowDocumentStore.name, 'IDLE'); ws.workflowExecutionStartedData = undefined; diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts index e317a237cf1..4046e88fd31 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts @@ -35,9 +35,11 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { serializeNode } from '@/app/utils/nodes/nodeTransforms'; import type { WorkflowObjectAccessors } from '../types'; import type { IWorkflowDb } from '@/Interface'; -import type { INode, IPinData } from 'n8n-workflow'; +import type { INode, IPinData, ProjectSharingData } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import type { WorkflowData } from '@n8n/rest-api-client/api/workflows'; +import type { Scope } from '@n8n/permissions'; +import type { IUsedCredential } from '@/features/credentials/credentials.types'; export { getPinDataSize, @@ -318,7 +320,10 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { }); } - function getSnapshot(): WorkflowObjectAccessors { + /** + * @deprecated use individual method or `getSnapshot()` + */ + function getWorkflowObjectAccessorSnapshot(): WorkflowObjectAccessors { return { id: workflowId, connectionsBySourceNode: workflowDocumentConnections.connectionsBySourceNode.value, @@ -333,6 +338,35 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { }; } + function getSnapshot(): IWorkflowDb { + return { + id: workflowId, + name: workflowDocumentName.name.value, + description: workflowDocumentDescription.description.value, + active: workflowDocumentActive.active.value, + activeVersionId: workflowDocumentActive.activeVersionId.value, + isArchived: workflowDocumentIsArchived.isArchived.value, + createdAt: workflowDocumentTimestamps.createdAt.value, + updatedAt: workflowDocumentTimestamps.updatedAt.value, + nodes: workflowDocumentNodes.allNodes.value, + connections: workflowDocumentConnections.connectionsBySourceNode.value, + settings: { ...DEFAULT_SETTINGS, ...workflowDocumentSettings.settings.value }, + tags: [...workflowDocumentTags.tags.value], + pinData: workflowDocumentPinData.pinData.value as IPinData, + sharedWithProjects: (workflowDocumentSharedWithProjects.sharedWithProjects.value ?? + []) as ProjectSharingData[], + homeProject: workflowDocumentHomeProject.homeProject.value ?? undefined, + scopes: workflowDocumentScopes.scopes.value as Scope[], + versionId: workflowDocumentVersionData.versionId.value, + usedCredentials: Object.values( + workflowDocumentUsedCredentials.usedCredentials.value, + ) as IUsedCredential[], + meta: workflowDocumentMeta.meta.value, + parentFolder: workflowDocumentParentFolder.parentFolder.value ?? undefined, + checksum: workflowDocumentChecksum.checksum.value, + }; + } + return { workflowId, workflowVersion, @@ -363,6 +397,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { hydrate, reset, getSnapshot, + getWorkflowObjectAccessorSnapshot, serialize, cloneWorkflowObject, createWorkflowObject, diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts index c2f9a4ee75a..f550e541d6e 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -1044,6 +1044,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { ); return { + /** + * @deprecated use granular methods or getSnapshot() in workflow document store. + */ workflow, currentWorkflowExecutions, workflowExecutionData, diff --git a/packages/frontend/editor-ui/src/app/views/NodeView.vue b/packages/frontend/editor-ui/src/app/views/NodeView.vue index fc515fb118f..1b79bb5ae8d 100644 --- a/packages/frontend/editor-ui/src/app/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/app/views/NodeView.vue @@ -246,8 +246,6 @@ const { fetchWorkflowDataFromUrl, resetWorkspace, initializeWorkspace, - editableWorkflow, - editableWorkflowObject, lastClickPosition, startChat, addNodesAndConnections, @@ -397,7 +395,11 @@ async function openWorkflow(data: IWorkflowDb) { */ const triggerNodes = computed(() => { - return editableWorkflow.value.nodes.filter((node) => nodeTypesStore.isTriggerNode(node.type)); + return ( + workflowDocumentStore?.value?.allNodes.filter((node) => + nodeTypesStore.isTriggerNode(node.type), + ) ?? [] + ); }); const containsTriggerNodes = computed(() => triggerNodes.value.length > 0); @@ -1209,13 +1211,15 @@ function onRunWorkflowButtonMouseLeave() { */ const chatTriggerNode = computed(() => { - return editableWorkflow.value.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE); + return workflowDocumentStore?.value?.allNodes.find( + (node) => node.type === CHAT_TRIGGER_NODE_TYPE, + ); }); const containsChatTriggerNodes = computed(() => { return ( !isExecutionWaitingForWebhook.value && - !!editableWorkflow.value.nodes.find( + !!workflowDocumentStore?.value?.allNodes.find( (node) => [MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) && node.disabled !== true, @@ -1277,7 +1281,9 @@ function onToggleChat() { * Evaluation */ const evaluationTriggerNode = computed(() => { - return editableWorkflow.value.nodes.find((node) => node.type === EVALUATION_TRIGGER_NODE_TYPE); + return workflowDocumentStore?.value?.allNodes.find( + (node) => node.type === EVALUATION_TRIGGER_NODE_TYPE, + ); }); /** @@ -1572,7 +1578,11 @@ watch([() => route.name, () => route.params.workflowId], () => { watch( () => { - return isLoading.value || isCanvasReadOnly.value || editableWorkflow.value.nodes.length !== 0; + return ( + isLoading.value || + isCanvasReadOnly.value || + (workflowDocumentStore?.value?.allNodes ?? []).length !== 0 + ); }, (isReadOnlyOrLoading) => { if (isReadOnlyOrLoading) { @@ -1785,11 +1795,9 @@ onBeforeUnmount(() => {