From d5d290d70661267a42e19aed170923dccff4b947 Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Mon, 11 May 2026 11:57:07 +0200 Subject: [PATCH] refactor(editor): Migrate workflow document store init (#30077) --- .../src/app/components/ActivationModal.vue | 9 +-- .../src/app/components/ChatEmbedModal.vue | 11 +--- .../components/FromAiParametersModal.test.ts | 3 +- .../app/components/FromAiParametersModal.vue | 9 +-- .../MainHeader/WorkflowPublishModal.vue | 9 +-- .../src/app/components/WorkflowSettings.vue | 26 +++------ .../components/WorkflowShareModal.ee.test.ts | 3 +- .../src/app/composables/useActivationError.ts | 11 +--- .../app/composables/useCanvasOperations.ts | 7 +-- .../src/app/composables/useDataSchema.test.ts | 19 +++++-- .../src/app/composables/useDataSchema.ts | 23 +++----- .../src/app/composables/useNodeDirtiness.ts | 9 +-- .../app/composables/useNodeExecution.test.ts | 1 + .../src/app/composables/useNodeExecution.ts | 9 +-- .../app/composables/useNodeHelpers.test.ts | 44 ++++++--------- .../src/app/composables/useNodeHelpers.ts | 28 +++------- .../src/app/composables/useNodeIconSource.ts | 11 +--- .../src/app/composables/usePinnedData.ts | 8 +-- .../handlers/nodeExecuteAfterData.ts | 16 +++++- .../handlers/workflowSettingsUpdated.test.ts | 1 + .../app/composables/useResolvedExpression.ts | 9 +-- .../app/composables/useRunWorkflow.test.ts | 2 + .../src/app/composables/useRunWorkflow.ts | 10 +--- .../app/composables/useToolParameters.test.ts | 3 +- .../src/app/composables/useToolParameters.ts | 9 +-- .../src/app/composables/useUniqueNodeName.ts | 22 +++----- .../app/composables/useWorkflowExtraction.ts | 43 ++++++--------- .../src/app/composables/useWorkflowHelpers.ts | 15 +---- .../app/composables/useWorkflowUpdate.test.ts | 1 + .../src/app/composables/useWorkflowUpdate.ts | 6 +- .../editor-ui/src/app/stores/canvas.store.ts | 20 +------ .../src/app/stores/workflowDocument.store.ts | 23 ++++++-- .../useWorkflowDocumentNodes.ts | 9 +++ .../src/app/stores/workflows.store.ts | 24 +++++--- .../ai/assistant/assistant.store.test.ts | 3 +- .../composables/useBuilderExecution.ts | 9 +-- .../composables/useBuilderSetupCards.test.ts | 29 +++++----- .../composables/useBuilderSetupCards.ts | 11 +--- .../assistant/composables/useBuilderTodos.ts | 11 +--- .../composables/useFocusedNodesChipUI.ts | 11 +--- .../composables/useNodeMention.test.ts | 2 + .../assistant/composables/useNodeMention.ts | 11 +--- .../composables/useReviewChanges.test.ts | 3 +- .../assistant/composables/useReviewChanges.ts | 9 +-- .../chatHub/components/CanvasChatOverlay.vue | 11 +--- .../components/InstanceAiWorkflowSetup.vue | 7 +-- .../useCredentialGroupSelection.ts | 13 +---- .../instanceAi/composables/useSetupActions.ts | 13 +---- .../composables/useSetupCardParameters.ts | 13 +---- .../instanceAi/composables/useSetupCards.ts | 11 +--- .../CredentialEdit/CredentialEdit.vue | 13 +---- .../WorkflowExecutionsLandingPage.vue | 9 +-- .../composables/useExecutionDebugging.test.ts | 1 + .../composables/useExecutionDebugging.ts | 9 +-- .../views/WorkflowExecutionsView.vue | 11 +--- .../execution/logs/components/LogsPanel.vue | 11 +--- .../logs/composables/useChatState.ts | 9 +-- .../useClearExecutionButtonVisible.ts | 9 +-- .../logs/composables/useLogsExecutionData.ts | 9 +-- .../logs/composables/useLogsSelection.ts | 11 +--- .../EventDestinationSettingsModal.vue | 11 +--- .../views/SettingsLogStreamingView.vue | 11 +--- .../components/ExpressionEditModal.vue | 9 +-- .../ndv/runData/schemaPreview.store.test.ts | 38 ++++--------- .../ndv/runData/schemaPreview.store.ts | 29 +++------- .../composables/useNodeSettingsParameters.ts | 13 +---- .../composables/useInstallNode.ts | 13 +---- .../components/cards/SetupCardBody.vue | 15 ++--- .../composables/useTriggerExecution.ts | 9 +-- ...useWorkflowSetupState.nodeGrouping.test.ts | 6 +- .../composables/useWorkflowSetupState.test.ts | 3 +- .../composables/useWorkflowSetupState.ts | 9 +-- .../commandBar/composables/useNodeCommands.ts | 9 +-- .../composables/useRecentResources.ts | 11 +--- .../composables/useWorkflowCommands.ts | 9 +-- .../components/CodeNodeEditor/AskAI/AskAI.vue | 12 ++-- .../composables/useActions.test.ts | 55 +++++++++---------- .../nodeCreator/composables/useActions.ts | 18 ++---- .../useGetNodeCreatorFilter.test.ts | 2 + .../composables/useGetNodeCreatorFilter.ts | 12 +--- .../nodeCreator/composables/useViewStacks.ts | 7 ++- .../shared/nodeCreator/nodeCreator.store.ts | 9 +-- .../src/features/shared/tags/tags.store.ts | 16 +----- .../canvas/components/WorkflowCanvas.vue | 11 +--- .../elements/buttons/CanvasControlButtons.vue | 9 ++- .../parts/CanvasNodeSettingsIcons.vue | 11 +--- .../render-types/parts/CanvasNodeTrigger.vue | 9 +-- .../canvas/composables/useCanvasMapping.ts | 9 +-- .../composables/useExpressionResolveCtx.ts | 9 +-- .../experimental/experimentalNdv.store.ts | 16 +----- .../useSetupWorkflowCredentialsModalState.ts | 16 +----- .../workflowDiff/useWorkflowDiff.test.ts | 16 +++--- .../workflows/workflowDiff/useWorkflowDiff.ts | 21 ++++--- 93 files changed, 390 insertions(+), 775 deletions(-) diff --git a/packages/frontend/editor-ui/src/app/components/ActivationModal.vue b/packages/frontend/editor-ui/src/app/components/ActivationModal.vue index b4ad2911695..b19910e3d14 100644 --- a/packages/frontend/editor-ui/src/app/components/ActivationModal.vue +++ b/packages/frontend/editor-ui/src/app/components/ActivationModal.vue @@ -18,18 +18,13 @@ import { } from '../constants'; import { N8nButton, N8nCheckbox, N8nText } from '@n8n/design-system'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '../stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store'; const checked = ref(false); const executionsStore = useExecutionsStore(); const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const uiStore = useUIStore(); const router = useRouter(); diff --git a/packages/frontend/editor-ui/src/app/components/ChatEmbedModal.vue b/packages/frontend/editor-ui/src/app/components/ChatEmbedModal.vue index 4336765dd19..a72ade5b58d 100644 --- a/packages/frontend/editor-ui/src/app/components/ChatEmbedModal.vue +++ b/packages/frontend/editor-ui/src/app/components/ChatEmbedModal.vue @@ -5,11 +5,7 @@ import { createEventBus } from '@n8n/utils/event-bus'; import Modal from './Modal.vue'; import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '../constants'; import { useRootStore } from '@n8n/stores/useRootStore'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import HtmlEditor from '@/features/shared/editors/components/HtmlEditor/HtmlEditor.vue'; import JsEditor from '@/features/shared/editors/components/JsEditor/JsEditor.vue'; import { useI18n } from '@n8n/i18n'; @@ -27,10 +23,7 @@ const props = withDefaults( const i18n = useI18n(); const rootStore = useRootStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); type ChatEmbedModalTabValue = 'cdn' | 'vue' | 'react' | 'other'; type ChatEmbedModalTab = { diff --git a/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts b/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts index dd1a2f0cf45..b7b1827acab 100644 --- a/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts +++ b/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts @@ -9,7 +9,7 @@ import { useProjectsStore } from '@/features/collaboration/projects/projects.sto import { useRouter } from 'vue-router'; import { NodeConnectionTypes } from 'n8n-workflow'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { nextTick } from 'vue'; +import { nextTick, shallowRef } from 'vue'; import { createTestWorkflow } from '@/__tests__/mocks'; import { type MockedStore, mockedStore } from '@/__tests__/utils'; @@ -27,6 +27,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), })); diff --git a/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.vue b/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.vue index 8954fdbebf6..253a3910952 100644 --- a/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.vue +++ b/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.vue @@ -4,10 +4,7 @@ import { useRunWorkflow } from '@/app/composables/useRunWorkflow'; import { useTelemetry } from '@/app/composables/useTelemetry'; import { FROM_AI_PARAMETERS_MODAL_KEY } from '@/app/constants'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useNDVStore } from '@/features/ndv/shared/ndv.store'; import type { FormFieldValueUpdate } from '@n8n/design-system'; import { N8nButton, N8nCallout, N8nFormInputs, N8nText } from '@n8n/design-system'; @@ -41,9 +38,7 @@ const telemetry = useTelemetry(); const ndvStore = useNDVStore(); const modalBus = createEventBus(); const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const router = useRouter(); const { runWorkflow } = useRunWorkflow({ router }); const agentRequestStore = useAgentRequestStore(); diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowPublishModal.vue b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowPublishModal.vue index 50c8718cd12..9a305e3fb51 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowPublishModal.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowPublishModal.vue @@ -26,19 +26,14 @@ import { useSettingsStore } from '@/app/stores/settings.store'; import type { INodeUi } from '@/Interface'; import type { IUsedCredential } from '@/features/credentials/credentials.types'; import WorkflowActivationErrorMessage from '@/app/components/WorkflowActivationErrorMessage.vue'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { generateVersionLabelFromId } from '@/features/workflows/workflowHistory/utils'; const modalBus = createEventBus(); const i18n = useI18n(); const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const credentialsStore = useCredentialsStore(); const settingsStore = useSettingsStore(); const { showMessage } = useToast(); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue index 1f303046e0a..2edd826f1b5 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue @@ -43,10 +43,7 @@ import { getResourcePermissions } from '@n8n/permissions'; import { useI18n } from '@n8n/i18n'; import { useTelemetry } from '@/app/composables/useTelemetry'; import { useDebounce } from '@/app/composables/useDebounce'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp'; import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; @@ -83,11 +80,7 @@ const sourceControlStore = useSourceControlStore(); const collaborationStore = useCollaborationStore(); const workflowsStore = useWorkflowsStore(); const workflowsListStore = useWorkflowsListStore(); -const workflowDocumentStore = computed(() => { - const wfId = workflowsStore.workflowId; - if (!wfId) return null; - return useWorkflowDocumentStore(createWorkflowDocumentId(wfId)); -}); +const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowsEEStore = useWorkflowsEEStore(); const nodeCreatorStore = useNodeCreatorStore(); const posthogStore = usePostHog(); @@ -192,7 +185,7 @@ const isMCPEnabled = computed( const readOnlyEnv = computed( () => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly, ); -const workflowName = computed(() => workflowDocumentStore.value?.name ?? ''); +const workflowName = computed(() => workflowDocumentStore.value.name); const workflowId = computed(() => workflowsStore.workflowId); const workflow = computed(() => workflowsListStore.getWorkflowById(workflowId.value)); const isSharingEnabled = computed( @@ -594,8 +587,8 @@ const saveSettings = async () => { delete data.settings.maxExecutionTimeout; isLoading.value = true; - data.versionId = workflowDocumentStore?.value?.versionId ?? ''; - data.expectedChecksum = workflowDocumentStore?.value?.checksum; + data.versionId = workflowDocumentStore.value.versionId; + data.expectedChecksum = workflowDocumentStore.value.checksum; try { await workflowsStore.updateWorkflow(String(route.params.workflowId), data); @@ -610,10 +603,9 @@ const saveSettings = async () => { Object.entries(workflowSettings.value).filter(([, value]) => value !== 'DEFAULT'), ); - const oldSettings = (workflowDocumentStore?.value?.getSettingsSnapshot() ?? - {}) as IWorkflowSettings; + const oldSettings = workflowDocumentStore.value.getSettingsSnapshot() as IWorkflowSettings; - workflowDocumentStore?.value?.setSettings(localWorkflowSettings); + workflowDocumentStore.value.setSettings(localWorkflowSettings); isLoading.value = false; @@ -739,8 +731,8 @@ onMounted(async () => { }); } - const workflowSettingsData = (workflowDocumentStore?.value?.getSettingsSnapshot() ?? - {}) as IWorkflowSettings; + const workflowSettingsData = + workflowDocumentStore.value.getSettingsSnapshot() as IWorkflowSettings; if (workflowSettingsData.timeSavedMode === undefined) { workflowSettingsData.timeSavedMode = 'fixed'; 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 55e35e10596..a12ee0f52c8 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 @@ -1,4 +1,4 @@ -import { reactive } from 'vue'; +import { reactive, shallowRef } from 'vue'; import { createTestingPinia } from '@pinia/testing'; import userEvent from '@testing-library/user-event'; import { waitFor } from '@testing-library/vue'; @@ -30,6 +30,7 @@ const mockWorkflowDocumentState = reactive({ }); vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: () => mockWorkflowDocumentState, + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentState), createWorkflowDocumentId: (id: string) => `${id}@latest`, })); diff --git a/packages/frontend/editor-ui/src/app/composables/useActivationError.ts b/packages/frontend/editor-ui/src/app/composables/useActivationError.ts index 2be506cc330..a58302112a9 100644 --- a/packages/frontend/editor-ui/src/app/composables/useActivationError.ts +++ b/packages/frontend/editor-ui/src/app/composables/useActivationError.ts @@ -1,20 +1,13 @@ import { computed, toValue, type MaybeRefOrGetter } from 'vue'; import { useI18n } from '@n8n/i18n'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '../stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store'; /** * Composable for activation error helpers. * Resolves a node ID to a formatted activation error message reactively. */ export function useActivationError(nodeId: MaybeRefOrGetter) { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const i18n = useI18n(); const errorMessage = computed(() => { diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index 6897ed18cf7..b71ceb7621b 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -138,9 +138,8 @@ import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store'; import { clearAllNodeResourceLocatorValues } from '@/features/workflows/templates/utils/templateTransforms'; import { useClipboard } from '@vueuse/core'; import { - useWorkflowDocumentStore, - createWorkflowDocumentId, pinDataToExecutionData, + injectWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; import { serializeNode } from '@/app/utils/nodes/nodeTransforms'; @@ -192,9 +191,7 @@ export function useCanvasOperations() { const templatesStore = useTemplatesStore(); const focusPanelStore = useFocusPanelStore(); const setupPanelStore = useSetupPanelStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const i18n = useI18n(); const toast = useToast(); 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 8c82d519fa1..aa56c1eefec 100644 --- a/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useDataSchema.test.ts @@ -13,14 +13,21 @@ import { import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { JSONSchema7 } from 'json-schema'; import { mock } from 'vitest-mock-extended'; +import { computed } from 'vue'; vi.mock('@/app/stores/workflows.store'); -vi.mock('@/app/stores/workflowDocument.store', () => ({ - createWorkflowDocumentId: vi.fn(() => 'test'), - useWorkflowDocumentStore: vi.fn(() => ({ - getSettingsSnapshot: () => ({ binaryMode: undefined }), - })), -})); +vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => { + const actual = await importOriginal>(); + return { + ...actual, + injectWorkflowDocumentStore: vi.fn(() => + computed(() => ({ + getSettingsSnapshot: () => ({ binaryMode: undefined }), + getNodePinData: () => 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 04fb4e03b88..b31377c8895 100644 --- a/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts +++ b/packages/frontend/editor-ui/src/app/composables/useDataSchema.ts @@ -8,10 +8,7 @@ import type { SchemaType, } from '@/Interface'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { generatePath, getNodeParentExpression } from '@/app/utils/mappingUtils'; import { isObject } from '@/app/utils/objectUtils'; import { isObj } from '@/app/utils/typeGuards'; @@ -26,12 +23,14 @@ import { type ITaskDataConnections, NodeConnectionTypes, } from 'n8n-workflow'; -import { computed, ref } from 'vue'; +import { 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() { + const workflowDocumentStore = injectWorkflowDocumentStore(); + function getSchema( input: Optional, path = '', @@ -211,11 +210,9 @@ export function useDataSchema() { ): INodeExecutionData[] { if (!node) return []; - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowsStore.workflowId), - ); - const pinnedData = workflowDocumentStore.getNodePinData(node.name)?.map((item) => item.json); + const pinnedData = workflowDocumentStore.value + .getNodePinData(node.name) + ?.map((item) => item.json); let inputData = getNodeInputData(node, runIndex, outputIndex); if (pinnedData) { @@ -396,6 +393,7 @@ const isEmptySchema = (schema: Schema) => { const prefixTitle = (title: string, prefix?: string) => (prefix ? `${prefix}[${title}]` : title); export const useFlattenSchema = () => { + const workflowDocumentStore = injectWorkflowDocumentStore(); const closedNodes = ref>(new Set()); const toggleNode = (id: string) => { if (closedNodes.value.has(id)) { @@ -555,11 +553,6 @@ export const useFlattenSchema = () => { return acc; } - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); - acc = acc.concat( flattenSchema({ isDataEmpty: item.isDataEmpty, diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeDirtiness.ts b/packages/frontend/editor-ui/src/app/composables/useNodeDirtiness.ts index 5825dc75986..b2dae09b2e5 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeDirtiness.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeDirtiness.ts @@ -9,10 +9,7 @@ import { } from '@/app/models/history'; import { useHistoryStore } from '@/app/stores/history.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { CanvasNodeDirtiness, type CanvasNodeDirtinessType, @@ -125,9 +122,7 @@ export function useNodeDirtiness() { const historyStore = useHistoryStore(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); function getIncomingConnections(nodeName: string): INodeConnections { return workflowDocumentStore.value.incomingConnectionsByNodeName(nodeName); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts index 03839a5538b..8a06071f567 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts @@ -83,6 +83,7 @@ const { vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), + injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }), })); vi.mock('vue-router', async (importOriginal) => { diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts index 08eadff5732..ab9b32ddd94 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts @@ -24,10 +24,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry'; import { useToast } from '@/app/composables/useToast'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; import { injectWorkflowState } from '@/app/composables/useWorkflowState'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { needsAgentInput } from '@/app/utils/nodes/nodeTransforms'; import { generateCodeForAiTransform } from '@/features/ndv/parameters/utils/buttonParameter.utils'; @@ -103,9 +100,7 @@ export function useNodeExecution( const uiStore = useUIStore(); const workflowState = injectWorkflowState(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router }); const nodeHelpers = useNodeHelpers(); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts index 3c04e28e285..be20e20130d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts @@ -1,3 +1,4 @@ +import { shallowRef } from 'vue'; import { setActivePinia } from 'pinia'; import type { ExecutionStatus, @@ -19,27 +20,29 @@ import { faker } from '@faker-js/faker'; import type { INodeUi } from '@/Interface'; import type { IUsedCredential } from '@/features/credentials/credentials.types'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const mockDocumentStoreUsedCredentials: Record = {}; +const mockDocumentStore = { + name: '', + settings: {}, + pinData: {}, + usedCredentials: mockDocumentStoreUsedCredentials, + allNodes: [], + workflowTriggerNodes: [], + getNodeByName: vi.fn(), + setNodeIssue: vi.fn(), + updateNodeProperties: vi.fn(), + getExpressionHandler: vi.fn(() => ({})), + getPinDataSnapshot: vi.fn().mockReturnValue({}), +}; + vi.mock('@/app/stores/workflowDocument.store', async () => { const actual = await vi.importActual('@/app/stores/workflowDocument.store'); return { ...actual, - useWorkflowDocumentStore: vi.fn(() => ({ - name: '', - settings: {}, - pinData: {}, - usedCredentials: mockDocumentStoreUsedCredentials, - allNodes: [], - workflowTriggerNodes: [], - getNodeByName: vi.fn(), - setNodeIssue: vi.fn(), - updateNodeProperties: vi.fn(), - getExpressionHandler: vi.fn(() => ({})), - getPinDataSnapshot: vi.fn().mockReturnValue({}), - })), + useWorkflowDocumentStore: vi.fn(() => mockDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockDocumentStore), }; }); @@ -77,18 +80,7 @@ describe('useNodeHelpers()', () => { parameters: {}, }; - vi.mocked(useWorkflowDocumentStore).mockReturnValueOnce({ - name: '', - settings: {}, - usedCredentials: mockDocumentStoreUsedCredentials, - allNodes: [], - workflowTriggerNodes: [], - getNodeByName: vi.fn().mockReturnValue(node), - setNodeIssue: vi.fn(), - updateNodeProperties: vi.fn(), - getExpressionHandler: vi.fn(() => ({})), - getPinDataSnapshot: vi.fn().mockReturnValue({}), - } as unknown as ReturnType); + mockDocumentStore.getNodeByName = vi.fn().mockReturnValue(node); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts index 528cbffaef9..cbe7de80c9e 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts @@ -1,4 +1,4 @@ -import { ref, computed } from 'vue'; +import { ref } from 'vue'; import { useHistoryStore } from '@/app/stores/history.store'; import { CUSTOM_API_CALL_KEY, @@ -48,10 +48,7 @@ import { useTelemetry } from './useTelemetry'; import { hasPermission } from '@/app/utils/rbac/permissions'; import { useCanvasStore } from '@/app/stores/canvas.store'; import { useSettingsStore } from '@/app/stores/settings.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; declare namespace HttpRequestNode { namespace V2 { @@ -72,9 +69,7 @@ export function useNodeHelpers() { const i18n = useI18n(); const canvasStore = useCanvasStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const isInsertingNodes = ref(false); const credentialsUpdated = ref(false); @@ -154,10 +149,7 @@ export function useNodeHelpers() { return []; } - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowsStore.workflowId), - ); - const usedCredentials = workflowDocumentStore.usedCredentials; + const usedCredentials = workflowDocumentStore.value.usedCredentials; return Object.values(credentials) .map(({ id }) => id) @@ -192,10 +184,7 @@ export function useNodeHelpers() { workflow: WorkflowObjectAccessors, ignoreIssues?: string[], ): INodeIssues | null { - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowsStore.workflowId), - ); - const pinDataNodeNames = Object.keys(workflowDocumentStore.pinData); + const pinDataNodeNames = Object.keys(workflowDocumentStore.value.pinData); let nodeIssues: INodeIssues | null = null; ignoreIssues = ignoreIssues ?? []; @@ -429,9 +418,6 @@ export function useNodeHelpers() { nodeType?: INodeTypeDescription, ): INodeIssues | null { const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion); - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowsStore.workflowId), - ); if (node.disabled) { // Node is disabled return null; @@ -470,7 +456,7 @@ export function useNodeHelpers() { // Prevents HTTP Request node from being unusable if a sharee does not have direct // access to a credential const isCredentialUsedInWorkflow = - workflowDocumentStore.usedCredentials?.[ + workflowDocumentStore.value.usedCredentials?.[ node.credentials?.[nodeCredentialType]?.id as string ]; @@ -558,7 +544,7 @@ export function useNodeHelpers() { if (nameMatches.length === 0) { const isCredentialUsedInWorkflow = - workflowDocumentStore.usedCredentials?.[selectedCredentials.id as string]; + workflowDocumentStore.value.usedCredentials?.[selectedCredentials.id as string]; if ( !isCredentialUsedInWorkflow && diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeIconSource.ts b/packages/frontend/editor-ui/src/app/composables/useNodeIconSource.ts index 6a629b5b1e2..3c44c4b8e7d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeIconSource.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeIconSource.ts @@ -1,20 +1,13 @@ import type { INode } from 'n8n-workflow'; import { getNodeIconSource, type IconNodeType, type NodeIconSource } from '../utils/nodeIcon'; import { computed, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'; -import { useWorkflowsStore } from '../stores/workflows.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '../stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store'; export function useNodeIconSource( nodeType: MaybeRefOrGetter, node?: MaybeRefOrGetter, ): ComputedRef { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); return computed(() => { const typeValue = toValue(nodeType); diff --git a/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts b/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts index 7e1f6b9cfa6..045325d2ef2 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePinnedData.ts @@ -19,8 +19,6 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useUIStore } from '@/app/stores/ui.store'; import { injectWorkflowDocumentStore, - useWorkflowDocumentStore, - createWorkflowDocumentId, getPinDataSize, pinDataToExecutionData, } from '@/app/stores/workflowDocument.store'; @@ -28,7 +26,7 @@ import type { INodeUi, IRunDataDisplayMode } from '@/Interface'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; import { useTelemetry } from '@/app/composables/useTelemetry'; import type { MaybeRef } from 'vue'; -import { computed, shallowRef, unref } from 'vue'; +import { computed, unref } from 'vue'; import { useRootStore } from '@n8n/stores/useRootStore'; import { useNodeType } from '@/app/composables/useNodeType'; import { useDataSchema } from './useDataSchema'; @@ -60,9 +58,7 @@ export function usePinnedData( const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); const uiStore = useUIStore(); - const workflowDocumentStore = - injectWorkflowDocumentStore() ?? - shallowRef(useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))); + const workflowDocumentStore = injectWorkflowDocumentStore(); const toast = useToast(); const i18n = useI18n(); const telemetry = useTelemetry(); diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.ts index 25224769aab..9e9a7aced31 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.ts @@ -1,15 +1,29 @@ import type { NodeExecuteAfterData } from '@n8n/api-types/push/execution'; import { useSchemaPreviewStore } from '@/features/ndv/runData/schemaPreview.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { computed } from 'vue'; +import { + createWorkflowDocumentId, + useWorkflowDocumentStore, +} from '@/app/stores/workflowDocument.store'; /** * Handles the 'nodeExecuteAfterData' event, which is sent after a node has executed and contains the resulting data. */ export async function nodeExecuteAfterData({ data: pushData }: NodeExecuteAfterData) { const workflowsStore = useWorkflowsStore(); + const workflowDocumentStore = computed(() => + useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), + ); const schemaPreviewStore = useSchemaPreviewStore(); workflowsStore.updateNodeExecutionRunData(pushData); - void schemaPreviewStore.trackSchemaPreviewExecution(pushData); + const node = workflowDocumentStore.value.getNodeByName(pushData.nodeName); + + if (!node) { + return; + } + + void schemaPreviewStore.trackSchemaPreviewExecution(workflowsStore.workflowId, node, pushData); } diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/workflowSettingsUpdated.test.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/workflowSettingsUpdated.test.ts index ad80a6077fe..c63c1a7595f 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/workflowSettingsUpdated.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/workflowSettingsUpdated.test.ts @@ -25,6 +25,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), createWorkflowDocumentId: (id: string) => id, + injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }), })); const makeEvent = ( diff --git a/packages/frontend/editor-ui/src/app/composables/useResolvedExpression.ts b/packages/frontend/editor-ui/src/app/composables/useResolvedExpression.ts index b425776a446..32962936e74 100644 --- a/packages/frontend/editor-ui/src/app/composables/useResolvedExpression.ts +++ b/packages/frontend/editor-ui/src/app/composables/useResolvedExpression.ts @@ -18,10 +18,7 @@ import { watch, } from 'vue'; import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { ExpressionLocalResolveContextSymbol } from '@/app/constants'; import type { ExpressionLocalResolveContext } from '@/app/types/expressions'; @@ -40,9 +37,7 @@ export function useResolvedExpression({ }) { const ndvStore = useNDVStore(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const { resolveExpression } = useWorkflowHelpers(); 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 a09e011f61c..e33ccaeb7f6 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts @@ -1,3 +1,4 @@ +import { shallowRef } from 'vue'; import { setActivePinia } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; import { useRouter } from 'vue-router'; @@ -87,6 +88,7 @@ const { mockDocumentStore } = vi.hoisted(() => { vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: () => mockDocumentStore, + injectWorkflowDocumentStore: () => shallowRef(mockDocumentStore), createWorkflowDocumentId: (id: string) => `${id}@latest`, })); diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts index f43673787a9..111748c14e7 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts @@ -37,10 +37,7 @@ import { import { useRootStore } from '@n8n/stores/useRootStore'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { displayForm } from '@/features/execution/executions/executions.utils'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers'; @@ -58,7 +55,6 @@ import { useCanvasOperations } from './useCanvasOperations'; import { chatEventBus } from '@n8n/chat/event-buses'; import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore'; import { useWorkflowSaving } from './useWorkflowSaving'; -import { computed } from 'vue'; import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import { useDocumentTitle } from './useDocumentTitle'; import { useChat } from '@n8n/chat/composables'; @@ -81,9 +77,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { const workflowsStore = useWorkflowsStore(); const workflowState = injectWorkflowState(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeHelpers = useNodeHelpers(); const workflowSaving = useWorkflowSaving({ diff --git a/packages/frontend/editor-ui/src/app/composables/useToolParameters.test.ts b/packages/frontend/editor-ui/src/app/composables/useToolParameters.test.ts index 86ace2c15d0..7e10cc5408b 100644 --- a/packages/frontend/editor-ui/src/app/composables/useToolParameters.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useToolParameters.test.ts @@ -1,7 +1,7 @@ import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { ref, nextTick } from 'vue'; +import { ref, shallowRef, nextTick } from 'vue'; import { waitFor } from '@testing-library/vue'; import { useToolParameters } from './useToolParameters'; import { useWorkflowsStore } from '../stores/workflows.store'; @@ -27,6 +27,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), })); diff --git a/packages/frontend/editor-ui/src/app/composables/useToolParameters.ts b/packages/frontend/editor-ui/src/app/composables/useToolParameters.ts index f2178d47631..987274e0a04 100644 --- a/packages/frontend/editor-ui/src/app/composables/useToolParameters.ts +++ b/packages/frontend/editor-ui/src/app/composables/useToolParameters.ts @@ -8,10 +8,7 @@ import { } from 'n8n-workflow'; import { computed, reactive, ref, watch, type Ref } from 'vue'; import { useWorkflowsStore } from '../stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; import { useNodeTypesStore } from '../stores/nodeTypes.store'; import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore'; @@ -35,9 +32,7 @@ export function useToolParameters({ node }: GetToolParametersProps) { const nodeTypesStore = useNodeTypesStore(); const agentRequestStore = useAgentRequestStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const selectedToolMap = reactive>({}); const error = ref(undefined); diff --git a/packages/frontend/editor-ui/src/app/composables/useUniqueNodeName.ts b/packages/frontend/editor-ui/src/app/composables/useUniqueNodeName.ts index de83993a8ed..b140d75a4d7 100644 --- a/packages/frontend/editor-ui/src/app/composables/useUniqueNodeName.ts +++ b/packages/frontend/editor-ui/src/app/composables/useUniqueNodeName.ts @@ -1,11 +1,9 @@ -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '../stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store'; export function useUniqueNodeName() { + const workflowDocumentStore = injectWorkflowDocumentStore(); + /** * All in-store node name defaults ending with a number, e.g. * `AWS S3`, `Magento 2`, `MSG91`, `S3`, `SIGNL4`, `sms77` @@ -27,11 +25,9 @@ export function useUniqueNodeName() { * all nodes on canvas and any extra names that cannot be used. */ function uniqueNodeName(originalName: string, extraNames: string[] = []) { - const { canvasNames } = useWorkflowDocumentStore( - createWorkflowDocumentId(useWorkflowsStore().workflowId), - ); - - const isUnique = !canvasNames.has(originalName) && !extraNames.includes(originalName); + const isUnique = + !workflowDocumentStore.value.canvasNames.has(originalName) && + !extraNames.includes(originalName); if (isUnique) return originalName; @@ -54,7 +50,7 @@ export function useUniqueNodeName() { unique = originalName; - while (canvasNames.has(unique) || extraNames.includes(unique)) { + while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) { unique = originalName + index++; } @@ -79,7 +75,7 @@ export function useUniqueNodeName() { unique = match.groups.base; - while (canvasNames.has(unique) || extraNames.includes(unique)) { + while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) { unique = match.groups.base + '-' + index++; } @@ -110,7 +106,7 @@ export function useUniqueNodeName() { unique = base; - while (canvasNames.has(unique) || extraNames.includes(unique)) { + while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) { unique = base + index++; } diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts index e291bac9004..1883991b8c9 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowExtraction.ts @@ -1,8 +1,5 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { buildAdjacencyList, parseExtractableSubgraphSelection, @@ -41,12 +38,7 @@ const CANVAS_HISTORY_OPTIONS = { export function useWorkflowExtraction() { const uiStore = useUIStore(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => { - if (workflowsStore.workflowId) { - return useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)); - } - return null; - }); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const toast = useToast(); const router = useRouter(); @@ -56,7 +48,7 @@ export function useWorkflowExtraction() { const telemetry = useTelemetry(); const adjacencyList = computed(() => - buildAdjacencyList(workflowDocumentStore.value?.connectionsBySourceNode ?? {}), + buildAdjacencyList(workflowDocumentStore.value.connectionsBySourceNode), ); function showError(message: string) { @@ -263,8 +255,8 @@ export function useWorkflowExtraction() { ...endNodeConnection, }, settings: { executionOrder: 'v1' }, - projectId: workflowDocumentStore.value?.homeProject?.id, - parentFolderId: workflowDocumentStore.value?.parentFolder?.id ?? undefined, + projectId: workflowDocumentStore.value.homeProject?.id, + parentFolderId: workflowDocumentStore.value.parentFolder?.id ?? undefined, }; } @@ -334,12 +326,11 @@ export function useWorkflowExtraction() { ...x: Parameters ) => ReturnType, ) => { - const node = workflowDocumentStore?.value?.getNodeByName(nodeName); + const node = workflowDocumentStore.value.getNodeByName(nodeName); if (!node) return true; // invariant broken -> abort onto error path const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion); if (!nodeType) return true; // invariant broken -> abort onto error path - const expression = workflowDocumentStore?.value?.getExpressionHandler(); - if (!expression) return true; + const expression = workflowDocumentStore.value.getExpressionHandler(); const ios = getIOs({ expression }, node, nodeType); return ( @@ -407,7 +398,7 @@ export function useWorkflowExtraction() { ); for (const node of selectionChildNodes) { - const currentNode = workflowDocumentStore?.value?.allNodes.find((x) => x.id === node.id); + const currentNode = workflowDocumentStore.value.allNodes.find((x) => x.id === node.id); if (isEqual(node, currentNode)) continue; @@ -425,7 +416,7 @@ export function useWorkflowExtraction() { function tryExtractNodesIntoSubworkflow(nodeIds: string[]): boolean { const subGraph = nodeIds - .map((id) => workflowDocumentStore?.value?.getNodeById(id)) + .map((id) => workflowDocumentStore.value.getNodeById(id)) .filter((x) => x !== undefined); const triggers = subGraph.filter((x) => @@ -461,7 +452,7 @@ export function useWorkflowExtraction() { ) { const { start, end } = selection; - const allNodeNames = workflowDocumentStore?.value?.allNodes.map((x) => x.name) ?? []; + const allNodeNames = workflowDocumentStore.value.allNodes.map((x) => x.name) ?? []; let startNodeName = 'Start'; const subGraphNames = subGraph.map((x) => x.name); @@ -471,16 +462,16 @@ export function useWorkflowExtraction() { while (subGraphNames.includes(returnNodeName)) returnNodeName += '_1'; const directAfterEndNodeNames = end - ? (workflowDocumentStore?.value - ?.getChildNodes(end, 'main', 1) - .map((x) => workflowDocumentStore?.value?.getNodeByName(x)?.name) + ? (workflowDocumentStore.value + .getChildNodes(end, 'main', 1) + .map((x) => workflowDocumentStore.value.getNodeByName(x)?.name) .filter((x) => x !== undefined) ?? []) : []; const allAfterEndNodes = end - ? (workflowDocumentStore?.value - ?.getChildNodes(end, 'ALL') - .map((x) => workflowDocumentStore?.value?.getNodeByName(x) ?? null) + ? (workflowDocumentStore.value + .getChildNodes(end, 'ALL') + .map((x) => workflowDocumentStore.value.getNodeByName(x) ?? null) .filter((x) => x !== null) ?? []) : []; @@ -507,7 +498,7 @@ export function useWorkflowExtraction() { newWorkflowName, selection, nodes, - workflowDocumentStore.value?.connectionsBySourceNode ?? {}, + workflowDocumentStore.value?.connectionsBySourceNode, variables, afterVariables, startNodeName, diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index e60af50af5f..74a0407d95f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -54,8 +54,8 @@ import type { ExpressionLocalResolveContext } from '@/app/types/expressions'; import { useWorkflowDocumentStore, createWorkflowDocumentId, + injectWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; -import { computed } from 'vue'; import type { WorkflowObjectAccessors } from '../types'; export type ResolveParameterOptions = { @@ -503,9 +503,7 @@ export function useWorkflowHelpers() { const i18n = useI18n(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); function getNodeTypesMaxCount() { const nodes = workflowDocumentStore.value.allNodes; @@ -858,14 +856,6 @@ export function useWorkflowHelpers() { return { workflowDocumentStore: initializedWorkflowDocumentStore }; } - /** - * Check if workflow contains any node from specified package - * by performing a quick check based on the node type name. - */ - const containsNodeFromPackage = (workflow: IWorkflowDb, packageName: string) => { - return workflow.nodes.some((node) => node.type.startsWith(packageName)); - }; - function getMethods(trigger: INode) { if (trigger.type === WEBHOOK_NODE_TYPE) { if (trigger.parameters.multipleMethods === true) { @@ -942,7 +932,6 @@ export function useWorkflowHelpers() { getWorkflowProjectRole, initState, getNodeParametersWithResolvedExpressions, - containsNodeFromPackage, checkConflictingWebhooks, }; } diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.test.ts index 9cdbc46d178..86f5826dbbf 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.test.ts @@ -55,6 +55,7 @@ const mockDocumentStore = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), + injectWorkflowDocumentStore: vi.fn().mockReturnValue({ value: mockDocumentStore }), })); // Mock useWorkflowState - using hoisted for proper initialization diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts index 7e57c74df58..ef224901b28 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts @@ -9,12 +9,12 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowDocumentStore, createWorkflowDocumentId, + injectWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useBuilderStore } from '@/features/ai/assistant/builder.store'; import { useUIStore } from '@/app/stores/ui.store'; -import { computed } from 'vue'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; @@ -50,9 +50,7 @@ export function useWorkflowUpdate() { const canvasOperations = useCanvasOperations(); const nodeHelpers = useNodeHelpers(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); /** * Categorize nodes into those to update, add, or remove. diff --git a/packages/frontend/editor-ui/src/app/stores/canvas.store.ts b/packages/frontend/editor-ui/src/app/stores/canvas.store.ts index 190cf697146..5c51722f7a4 100644 --- a/packages/frontend/editor-ui/src/app/stores/canvas.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/canvas.store.ts @@ -1,30 +1,13 @@ import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; -import type { INodeUi, XYPosition } from '@/Interface'; +import type { XYPosition } from '@/Interface'; import { useLoadingService } from '@/app/composables/useLoadingService'; export const useCanvasStore = defineStore('canvas', () => { - const workflowStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowStore.workflowId)), - ); const loadingService = useLoadingService(); const newNodeInsertPosition = ref(null); - const nodes = computed(() => workflowDocumentStore.value?.allNodes ?? []); - const aiNodes = computed(() => - nodes.value.filter( - (node) => - node.type.includes('langchain') || - (node.type === 'n8n-nodes-base.evaluation' && node.parameters?.operation === 'setMetrics'), - ), - ); const hasRangeSelection = ref(false); function setHasRangeSelection(value: boolean) { @@ -34,7 +17,6 @@ export const useCanvasStore = defineStore('canvas', () => { return { newNodeInsertPosition, isLoading: loadingService.isLoading, - aiNodes, hasRangeSelection: computed(() => hasRangeSelection.value), startLoading: loadingService.startLoading, setLoadingText: loadingService.setLoadingText, 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 4046e88fd31..d240db235b8 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts @@ -1,6 +1,6 @@ import { defineStore, getActivePinia } from 'pinia'; import { STORES } from '@n8n/stores'; -import { inject } from 'vue'; +import { computed, inject, type ShallowRef } from 'vue'; import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; import { useWorkflowDocumentActive } from './workflowDocument/useWorkflowDocumentActive'; import { useWorkflowDocumentHomeProject } from './workflowDocument/useWorkflowDocumentHomeProject'; @@ -40,6 +40,7 @@ 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'; +import { useWorkflowsStore } from './workflows.store'; export { getPinDataSize, @@ -423,11 +424,23 @@ export function disposeWorkflowDocumentStore(store: ReturnType +> { + const workflowsStore = useWorkflowsStore(); + const fallback = computed(() => { + // TODO: once usages outside of a component tree is eliminated, + // this can be replaced with useWorkflowId() + const fallbackWorkflowId = workflowsStore.workflowId; + + return useWorkflowDocumentStore(createWorkflowDocumentId(fallbackWorkflowId)); + }); + const injected = inject(WorkflowDocumentStoreKey, null); + + return computed(() => injected?.value ?? fallback.value); } diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentNodes.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentNodes.ts index 67c9463e84e..90e373b9bab 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentNodes.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentNodes.ts @@ -193,6 +193,14 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) { }), ); + const aiNodes = computed(() => + nodes.value.filter( + (node) => + node.type.includes('langchain') || + (node.type === 'n8n-nodes-base.evaluation' && node.parameters?.operation === 'setMetrics'), + ), + ); + function getNodeById(id: string): INodeUi | undefined { return nodes.value.find((node) => node.id === id); } @@ -469,6 +477,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) { nodesByName, canvasNames, workflowTriggerNodes, + aiNodes, getNodeById, getNodeByName, findNodeByPartialId, 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 b258b6c6448..b822fbb6708 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -41,7 +41,6 @@ import { computed, ref } from 'vue'; import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; import type { ExecutionRedactionQueryDto, PushPayload } from '@n8n/api-types'; import { useTelemetry } from '@/app/composables/useTelemetry'; -import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers'; import { useSettingsStore } from './settings.store'; import { useUsersStore } from '@/features/settings/users/users.store'; import { updateCurrentUserSettings } from '@n8n/rest-api-client/api/users'; @@ -55,12 +54,13 @@ import { createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; import { getPairedItemsMapping } from '@/app/utils/pairedItemUtils'; +import { useNodeTypesStore } from './nodeTypes.store'; export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const uiStore = useUIStore(); const telemetry = useTelemetry(); - const workflowHelpers = useWorkflowHelpers(); const settingsStore = useSettingsStore(); + const nodeTypesStore = useNodeTypesStore(); const rootStore = useRootStore(); const usersStore = useUsersStore(); const sourceControlStore = useSourceControlStore(); @@ -79,6 +79,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const chatPartialExecutionDestinationNode = ref(null); const selectedTriggerNodeName = ref(); + /** + * @deprecated use useWorkflowId() in Vue components/composables instead. + */ const workflowId = ref(''); // A workflow is new if it hasn't been saved to the backend yet. @@ -462,7 +465,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { node_graph_string: JSON.stringify( TelemetryHelpers.generateNodesGraph( workflowDocumentStore.serialize(), - workflowHelpers.getNodeTypes(), + nodeTypesStore.getAllNodeTypes(), { isCloudDeployment: settingsStore.isCloudDeployment, }, @@ -620,6 +623,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { return response && unflattenExecutionData(response); } + /** + * Check if workflow contains any node from specified package + * by performing a quick check based on the node type name. + */ + function containsNodeFromPackage(workflow: IWorkflowDb, packageName: string) { + return workflow.nodes.some((node) => node.type.startsWith(packageName)); + } + /** * Creates a new workflow with the provided data. * Ensures that the new workflow is not active upon creation. @@ -648,10 +659,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { sendData as unknown as IDataObject, ); - const isAIWorkflow = workflowHelpers.containsNodeFromPackage( - newWorkflow, - AI_NODES_PACKAGE_NAME, - ); + const isAIWorkflow = containsNodeFromPackage(newWorkflow, AI_NODES_PACKAGE_NAME); if (isAIWorkflow && !usersStore.isEasyAIWorkflowOnboardingDone) { await updateCurrentUserSettings(rootStore.restApiContext, { easyAIWorkflowOnboarded: true, @@ -693,7 +701,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { } if ( - workflowHelpers.containsNodeFromPackage(updatedWorkflow, AI_NODES_PACKAGE_NAME) && + containsNodeFromPackage(updatedWorkflow, AI_NODES_PACKAGE_NAME) && !usersStore.isEasyAIWorkflowOnboardingDone ) { await updateCurrentUserSettings(rootStore.restApiContext, { diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/assistant.store.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/assistant.store.test.ts index 36f713cf947..82d7f64a284 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/assistant.store.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/assistant.store.test.ts @@ -18,7 +18,7 @@ import { defaultSettings } from '@/__tests__/defaults'; import merge from 'lodash/merge'; import { DEFAULT_POSTHOG_SETTINGS } from '@/app/stores/posthog.store.test'; import { VIEWS } from '@/app/constants'; -import { reactive } from 'vue'; +import { reactive, shallowRef } from 'vue'; import * as chatAPI from '@/features/ai/assistant/assistant.api'; import * as telemetryModule from '@/app/composables/useTelemetry'; import type { Telemetry } from '@/app/plugins/telemetry'; @@ -39,6 +39,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), })); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderExecution.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderExecution.ts index d1ce29212b5..c8edce72258 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderExecution.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderExecution.ts @@ -8,10 +8,7 @@ import { useLogsStore } from '@/app/stores/logs.store'; import { useRunWorkflow } from '@/app/composables/useRunWorkflow'; import { useToast } from '@/app/composables/useToast'; import { isChatNode } from '@/app/utils/aiUtils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const RUNNING_STATES: string[] = ['running', 'waiting']; @@ -25,9 +22,7 @@ export function useBuilderExecution(isReady: ComputedRef) { const router = useRouter(); const i18n = useI18n(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const logsStore = useLogsStore(); const toast = useToast(); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.test.ts index 26f23f77330..5c5f2ba2d39 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.test.ts @@ -33,29 +33,30 @@ vi.mock('@/features/ai/assistant/builder.store', () => ({ useBuilderStore: () => mockBuilderStoreState, })); -const mockUnpinNodeData = vi.fn(); -const mockTouchPinnedDataLastRemovedAt = vi.fn(); -const mockAllNodes = ref([]); +const { mockDocumentStore } = vi.hoisted(() => ({ + mockDocumentStore: { + pinData: {}, + unpinNodeData: vi.fn(), + touchPinnedDataLastRemovedAt: vi.fn(), + allNodes: [] as INodeUi[], + name: '', + settings: {}, + getPinDataSnapshot: () => ({}), + }, +})); vi.mock('@/app/stores/workflows.store', () => ({ useWorkflowsStore: () => ({ workflowId: 'test-workflow-id', get allNodes() { - return mockAllNodes.value; + return mockDocumentStore.allNodes; }, }), })); vi.mock('@/app/stores/workflowDocument.store', () => ({ - useWorkflowDocumentStore: () => ({ - pinData: {}, - unpinNodeData: mockUnpinNodeData, - touchPinnedDataLastRemovedAt: mockTouchPinnedDataLastRemovedAt, - allNodes: [], - name: '', - settings: {}, - getPinDataSnapshot: () => ({}), - }), + useWorkflowDocumentStore: () => mockDocumentStore, createWorkflowDocumentId: (id: string) => id, + injectWorkflowDocumentStore: () => ({ value: mockDocumentStore }), })); vi.mock('@/app/stores/ui.store', () => ({ @@ -119,7 +120,7 @@ describe('useBuilderSetupCards', () => { currentScope = undefined; vi.clearAllMocks(); mockSetupCards.value = []; - mockAllNodes.value = []; + mockDocumentStore.allNodes = []; mockFirstTriggerName.value = null; mockBuilderStoreState.wizardCurrentStep = 0; mockBuilderStoreState.wizardHasExecutedWorkflow = false; diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.ts index c8980f73567..853f31e9ef9 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderSetupCards.ts @@ -4,19 +4,12 @@ import type { SetupCardItem } from '@/features/setupPanel/setupPanel.types'; import { isCardComplete } from '@/features/setupPanel/setupPanel.utils'; import { useWorkflowSetupState } from '@/features/setupPanel/composables/useWorkflowSetupState'; import { useBuilderStore } from '@/features/ai/assistant/builder.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { findPlaceholderDetails } from '@/features/ai/assistant/composables/useBuilderTodos'; export function useBuilderSetupCards() { const builderStore = useBuilderStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); // Sticky map of node name → placeholder parameter names. // Once a node's placeholders are detected, the entry persists even after the user diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderTodos.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderTodos.ts index 12564a01e95..0965ccc245c 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderTodos.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useBuilderTodos.ts @@ -1,10 +1,6 @@ import { computed } from 'vue'; import { useI18n } from '@n8n/i18n'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import type { WorkflowValidationIssue } from '@/Interface'; import { extractPlaceholderLabels, @@ -39,12 +35,9 @@ export interface TodosTrackingPayload { * used by the AI builder. */ export function useBuilderTodos() { - const workflowsStore = useWorkflowsStore(); const locale = useI18n(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); /** * Checks if a node is disabled, either directly or through any ancestor node. diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useFocusedNodesChipUI.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useFocusedNodesChipUI.ts index db445880138..8b884611921 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useFocusedNodesChipUI.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useFocusedNodesChipUI.ts @@ -1,11 +1,7 @@ import { computed } from 'vue'; import { useFocusedNodesStore } from '../focusedNodes.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; /** Threshold at which individual chips are bundled into a single count chip */ @@ -18,10 +14,7 @@ export const CHIP_BUNDLE_THRESHOLD = 3; export function useFocusedNodesChipUI() { const focusedNodesStore = useFocusedNodesStore(); const nodeTypesStore = useNodeTypesStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const confirmedNodes = computed(() => focusedNodesStore.confirmedNodes); const unconfirmedNodes = computed(() => focusedNodesStore.filteredUnconfirmedNodes); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.test.ts index 8f7b0a80f06..95e9d561037 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.test.ts @@ -1,3 +1,4 @@ +import { shallowRef } from 'vue'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { setActivePinia } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; @@ -33,6 +34,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), })); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.ts index a5917c32023..9597b2d1056 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useNodeMention.ts @@ -1,11 +1,7 @@ import { ref, computed, watch, type Ref } from 'vue'; import type { INodeUi } from '@/Interface'; import { useFocusedNodesStore } from '../focusedNodes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export interface UseNodeMentionOptions { maxResults?: number; @@ -37,10 +33,7 @@ export function useNodeMention(options: UseNodeMentionOptions = {}): UseNodeMent const { maxResults = 50 } = options; const focusedNodesStore = useFocusedNodesStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const showDropdown = ref(false); const searchQuery = ref(''); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.test.ts index 912532f85bb..0be4e6173b4 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.test.ts @@ -1,5 +1,5 @@ import { createPinia, setActivePinia } from 'pinia'; -import { defineComponent, nextTick, reactive } from 'vue'; +import { defineComponent, nextTick, reactive, shallowRef } from 'vue'; import { mount } from '@vue/test-utils'; import { useReviewChanges } from './useReviewChanges'; import type { INode, IConnections } from 'n8n-workflow'; @@ -97,6 +97,7 @@ vi.mock('@/app/stores/workflows.store', () => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: () => mockWorkflowDocumentStore, + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: () => 'test-id', })); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts index 9de65a7f169..06cb2413b91 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts @@ -10,10 +10,7 @@ import { createEventBus } from '@n8n/utils/event-bus'; import { useI18n } from '@n8n/i18n'; import { useBuilderStore } from '@/features/ai/assistant/builder.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useWorkflowHistoryStore } from '@/features/workflows/workflowHistory/workflowHistory.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUIStore } from '@/app/stores/ui.store'; @@ -44,9 +41,7 @@ export function useReviewChanges() { const posthogStore = usePostHog(); const i18n = useI18n(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const isLoadingDiff = ref(false); const cachedVersionNodes = computed(() => { const version = builderStore.latestRevertVersion; diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/CanvasChatOverlay.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/CanvasChatOverlay.vue index 73962ccef61..6379b17455f 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/CanvasChatOverlay.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/CanvasChatOverlay.vue @@ -2,20 +2,13 @@ import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'; import { useRoute } from 'vue-router'; import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { usePopOutWindow } from '@/features/execution/logs/composables/usePopOutWindow'; import CanvasChatFloatingWindow from './CanvasChatFloatingWindow.vue'; const route = useRoute(); const chatHubPanelStore = useChatHubPanelStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const canvasChatFloatingWindowRef = ref>(); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowSetup.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowSetup.vue index f7b59545642..7d07d0ffa2d 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowSetup.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowSetup.vue @@ -2,10 +2,10 @@ import { getWorkflow as fetchWorkflowApi } from '@/app/api/workflows'; import { ExpressionLocalResolveContextSymbol } from '@/app/constants'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowDocumentStore, createWorkflowDocumentId, + injectWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; import { getAppNameFromCredType } from '@/app/utils/nodeTypesUtils'; import { useWizardNavigation } from '@/features/ai/shared/composables/useWizardNavigation'; @@ -54,12 +54,9 @@ const props = defineProps<{ const i18n = useI18n(); const store = useInstanceAiStore(); const credentialsStore = useCredentialsStore(); -const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); const rootStore = useRootStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); // --------------------------------------------------------------------------- // Composable wiring — order matters for dependencies diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useCredentialGroupSelection.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useCredentialGroupSelection.ts index 279eaee9553..c8fced608bc 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useCredentialGroupSelection.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useCredentialGroupSelection.ts @@ -1,15 +1,11 @@ import type { ComputedRef } from 'vue'; -import { computed, ref } from 'vue'; +import { ref } from 'vue'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUIStore } from '@/app/stores/ui.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { getMainAuthField } from '@/app/utils/nodeTypesUtils'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { credGroupKey, type SetupCard } from '../instanceAiWorkflowSetup.utils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useCredentialGroupSelection( cards: ComputedRef, @@ -17,10 +13,7 @@ export function useCredentialGroupSelection( projectId?: string, ) { const uiStore = useUIStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const credentialsStore = useCredentialsStore(); const nodeTypesStore = useNodeTypesStore(); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupActions.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupActions.ts index f33fbda6843..33cd60ba4fc 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupActions.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupActions.ts @@ -1,17 +1,13 @@ import type { ComputedRef, Ref } from 'vue'; -import { ref, watch, onUnmounted, computed } from 'vue'; +import { ref, watch, onUnmounted } from 'vue'; import type { InstanceAiToolCallState, InstanceAiWorkflowSetupNode } from '@n8n/api-types'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { useTelemetry } from '@/app/composables/useTelemetry'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { INodeUi } from '@/Interface'; import { useRootStore } from '@n8n/stores/useRootStore'; import type { useInstanceAiStore } from '../instanceAi.store'; import type { DisplayCard, SetupCard } from '../instanceAiWorkflowSetup.utils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useSetupActions(deps: { requestId: Ref; @@ -34,10 +30,7 @@ export function useSetupActions(deps: { onApplySuccess?: () => void; }) { const telemetry = useTelemetry(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeHelpers = useNodeHelpers(); const isSubmitted = ref(false); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCardParameters.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCardParameters.ts index 3af3352fe46..04127724b0c 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCardParameters.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCardParameters.ts @@ -1,15 +1,11 @@ import type { ComputedRef, Ref } from 'vue'; -import { computed, ref } from 'vue'; +import { ref } from 'vue'; import { hasPlaceholderDeep } from '@n8n/utils'; import { NodeHelpers, type INodeProperties } from 'n8n-workflow'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { IUpdateInformation } from '@/Interface'; import { isNestedParam, isParamValueSet, type SetupCard } from '../instanceAiWorkflowSetup.utils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; /** Check if the original node parameter value was a placeholder sentinel. */ function isOriginalValuePlaceholder(req: SetupCard['nodes'][0], paramName: string): boolean { @@ -21,10 +17,7 @@ export function useSetupCardParameters( trackedParamNames: Ref>>, cardHasParamWork: (card: SetupCard) => boolean, ) { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const paramValues = ref>>({}); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCards.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCards.ts index f0acc5e7cd3..b28187256d8 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCards.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/composables/useSetupCards.ts @@ -4,7 +4,6 @@ import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types'; import { hasPlaceholderDeep } from '@n8n/utils'; import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { getNodeParametersIssues } from '@/features/setupPanel/setupPanel.utils'; import { @@ -14,20 +13,14 @@ import { type SetupCard, type SetupCardGroup, } from '../instanceAiWorkflowSetup.utils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useSetupCards( setupRequests: Ref, getCardCredentialId: (card: SetupCard) => string | null, isCredentialTypeTestable: (name: string) => boolean, ) { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const credentialsStore = useCredentialsStore(); diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialEdit.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialEdit.vue index 483ccfa0656..6285c971111 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialEdit.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialEdit.vue @@ -31,10 +31,7 @@ import { useNDVStore } from '@/features/ndv/shared/ndv.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useUIStore } from '@/app/stores/ui.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { Project, ProjectSharingData } from '@/features/collaboration/projects/projects.types'; import { getResourcePermissions } from '@n8n/permissions'; @@ -129,11 +126,7 @@ const useCustomOAuth = ref(false); const pendingAuthType = ref(null); const credentialDataCache = ref>({}); -const workflowDocumentStore = computed(() => - workflowsStore.workflowId - ? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)) - : undefined, -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const contextNode = computed(() => { if (ndvStore.activeNode) return ndvStore.activeNode; @@ -893,7 +886,7 @@ async function saveCredential(): Promise { } const appliedAuthType = pendingAuthType.value; - if (appliedAuthType && contextNode.value && workflowDocumentStore.value) { + if (appliedAuthType && contextNode.value) { updateNodeAuthType( workflowDocumentStore.value.updateNodeProperties, contextNode.value, diff --git a/packages/frontend/editor-ui/src/features/execution/executions/components/workflow/WorkflowExecutionsLandingPage.vue b/packages/frontend/editor-ui/src/features/execution/executions/components/workflow/WorkflowExecutionsLandingPage.vue index 11d502fbe39..b465120c088 100644 --- a/packages/frontend/editor-ui/src/features/execution/executions/components/workflow/WorkflowExecutionsLandingPage.vue +++ b/packages/frontend/editor-ui/src/features/execution/executions/components/workflow/WorkflowExecutionsLandingPage.vue @@ -9,10 +9,7 @@ import WorkflowExecutionsInfoAccordion from './WorkflowExecutionsInfoAccordion.v import { useI18n } from '@n8n/i18n'; import { N8nButton, N8nHeading, N8nText } from '@n8n/design-system'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const router = useRouter(); const route = useRoute(); const locale = useI18n(); @@ -20,9 +17,7 @@ const locale = useI18n(); const workflowId = useInjectWorkflowId(); const uiStore = useUIStore(); const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const executionCount = computed(() => workflowsStore.currentWorkflowExecutions.length); const containsTrigger = computed(() => workflowDocumentStore.value.workflowTriggerNodes.length > 0); diff --git a/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.test.ts b/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.test.ts index f5632e5b767..d90505a004e 100644 --- a/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.test.ts +++ b/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.test.ts @@ -48,6 +48,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ vi.mock('@/app/stores/workflowDocument.store', () => ({ useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), + injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }), })); let workflowState: WorkflowState; diff --git a/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.ts b/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.ts index 7b3784bef21..80d3af69714 100644 --- a/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.ts +++ b/packages/frontend/editor-ui/src/features/execution/executions/composables/useExecutionDebugging.ts @@ -8,10 +8,7 @@ import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/app/constants' import { DEBUG_PAYWALL_MODAL_KEY } from '../executions.constants'; import type { INodeUi } from '@/Interface'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useTelemetry } from '@/app/composables/useTelemetry'; @@ -34,9 +31,7 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) => const message = useMessage(); const toast = useToast(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowState = providedWorkflowState ?? injectWorkflowState(); const settingsStore = useSettingsStore(); const uiStore = useUIStore(); diff --git a/packages/frontend/editor-ui/src/features/execution/executions/views/WorkflowExecutionsView.vue b/packages/frontend/editor-ui/src/features/execution/executions/views/WorkflowExecutionsView.vue index fc0276e9036..ea0dfd678df 100644 --- a/packages/frontend/editor-ui/src/features/execution/executions/views/WorkflowExecutionsView.vue +++ b/packages/frontend/editor-ui/src/features/execution/executions/views/WorkflowExecutionsView.vue @@ -5,7 +5,6 @@ import { useExecutionsStore } from '../executions.store'; import { useI18n } from '@n8n/i18n'; import type { ExecutionFilterType } from '../executions.types'; import type { IWorkflowDb } from '@/Interface'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { NO_NETWORK_ERROR_CODE } from '@n8n/rest-api-client'; import { useToast } from '@/app/composables/useToast'; @@ -16,16 +15,10 @@ import type { ExecutionSummary } from 'n8n-workflow'; import { useDebounce } from '@/app/composables/useDebounce'; import { useTelemetry } from '@/app/composables/useTelemetry'; import { executionRetryMessage } from '../executions.utils'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const executionsStore = useExecutionsStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowsListStore = useWorkflowsListStore(); const i18n = useI18n(); const telemetry = useTelemetry(); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue index 182a66e749c..19ba25c0724 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue +++ b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue @@ -15,11 +15,7 @@ import { useLogsStore } from '@/app/stores/logs.store'; import { useLogsPanelLayout } from '@/features/execution/logs/composables/useLogsPanelLayout'; import { type KeyMap } from '@/app/composables/useKeybindings'; import LogsViewKeyboardEventListener from './LogsViewKeyboardEventListener.vue'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { N8nResizeWrapper } from '@n8n/design-system'; const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false }); @@ -31,10 +27,7 @@ const popOutContent = useTemplateRef('popOutContent'); const logsStore = useLogsStore(); const ndvStore = injectNDVStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowName = computed(() => workflowDocumentStore.value.name); const { diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.ts index 3faf8bbb3f3..c7b0543b37f 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.ts @@ -3,10 +3,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { useRunWorkflow } from '@/app/composables/useRunWorkflow'; import { VIEWS } from '@/app/constants'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useRootStore } from '@n8n/stores/useRootStore'; import MessageWithButtons from '@n8n/chat/components/MessageWithButtons.vue'; import { chatEventBus } from '@n8n/chat/event-buses'; @@ -47,9 +44,7 @@ export function useChatState( ): ChatState { const locale = useI18n(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowState = injectWorkflowState(); const rootStore = useRootStore(); const logsStore = useLogsStore(); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useClearExecutionButtonVisible.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useClearExecutionButtonVisible.ts index a55ac5360fc..5497135e6d2 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useClearExecutionButtonVisible.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useClearExecutionButtonVisible.ts @@ -3,18 +3,13 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useClearExecutionButtonVisible() { const route = useRoute(); const sourceControlStore = useSourceControlStore(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData); const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning); const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.ts index 943d29ec769..e3cc6828872 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.ts @@ -2,10 +2,7 @@ import { watch, computed, ref, type ComputedRef } from 'vue'; import type { IExecutionResponse } from '@/features/execution/executions/executions.types'; import { Workflow, type IRunExecutionData, type ITaskStartedData } from 'n8n-workflow'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { copyExecutionData, @@ -35,9 +32,7 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData const nodeHelpers = useNodeHelpers(); const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowState = injectWorkflowState(); const toast = useToast(); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsSelection.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsSelection.ts index 82de2497d8b..890c9d7509f 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsSelection.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsSelection.ts @@ -15,11 +15,7 @@ import { useUIStore } from '@/app/stores/ui.store'; import { shallowRef, watch } from 'vue'; import { computed } from 'vue'; import type { Ref, ComputedRef } from 'vue'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useLogsSelection( execution: ComputedRef, @@ -37,10 +33,7 @@ export function useLogsSelection( const logsStore = useLogsStore(); const uiStore = useUIStore(); const canvasStore = useCanvasStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); function syncSelectionToCanvasIfEnabled(value: LogEntry) { if (!logsStore.isLogSelectionSyncedWithCanvas) { diff --git a/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/components/EventDestinationSettingsModal.vue b/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/components/EventDestinationSettingsModal.vue index fe1cfc345ae..a42b45b5035 100644 --- a/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/components/EventDestinationSettingsModal.vue +++ b/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/components/EventDestinationSettingsModal.vue @@ -29,11 +29,7 @@ import { createEventBus } from '@n8n/utils/event-bus'; import { useLogStreamingStore } from '../logStreaming.store'; import { useNDVStore } from '@/features/ndv/shared/ndv.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue'; import type { IMenuItem, IUpdateInformation, ModalKey } from '@/Interface'; import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/app/constants'; @@ -88,10 +84,7 @@ const { confirm } = useMessage(); const telemetry = useTelemetry(); const logStreamingStore = useLogStreamingStore(); const ndvStore = useNDVStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const uiStore = useUIStore(); const unchanged = ref(!isNew); diff --git a/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/views/SettingsLogStreamingView.vue b/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/views/SettingsLogStreamingView.vue index 332b65b357c..de3ccb8c9c4 100644 --- a/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/views/SettingsLogStreamingView.vue +++ b/packages/frontend/editor-ui/src/features/integrations/logStreaming.ee/views/SettingsLogStreamingView.vue @@ -14,11 +14,7 @@ import { createEventBus } from '@n8n/utils/event-bus'; import { useDocumentTitle } from '@/app/composables/useDocumentTitle'; import { useI18n } from '@n8n/i18n'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { ElCol, ElRow, ElSwitch } from 'element-plus'; import { N8nActionBox, N8nButton, N8nHeading, N8nInfoTip, N8nNotice } from '@n8n/design-system'; @@ -26,10 +22,7 @@ const environment = process.env.NODE_ENV; const settingsStore = useSettingsStore(); const logStreamingStore = useLogStreamingStore(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const uiStore = useUIStore(); const credentialsStore = useCredentialsStore(); const documentTitle = useDocumentTitle(); diff --git a/packages/frontend/editor-ui/src/features/ndv/parameters/components/ExpressionEditModal.vue b/packages/frontend/editor-ui/src/features/ndv/parameters/components/ExpressionEditModal.vue index 9f82f0b5ede..4dc96851941 100644 --- a/packages/frontend/editor-ui/src/features/ndv/parameters/components/ExpressionEditModal.vue +++ b/packages/frontend/editor-ui/src/features/ndv/parameters/components/ExpressionEditModal.vue @@ -33,10 +33,7 @@ import { N8nText, type ResizeData, } from '@n8n/design-system'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const DEFAULT_LEFT_SIDEBAR_WIDTH = 360; type Props = { @@ -62,9 +59,7 @@ const emit = defineEmits<{ const ndvStore = injectNDVStore(); const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const telemetry = useTelemetry(); const i18n = useI18n(); diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.test.ts b/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.test.ts index cded240944e..d08d0347ead 100644 --- a/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.test.ts +++ b/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.test.ts @@ -23,21 +23,18 @@ vi.mock('@n8n/stores/useRootStore', () => ({ })), })); -vi.mock('@/app/stores/workflows.store', () => ({ - useWorkflowsStore: vi.fn(() => ({ - workflowId: '123', - })), -})); - const { mockGetNodeByName } = vi.hoisted(() => ({ mockGetNodeByName: vi.fn(), })); +const mockDocumentStore = vi.hoisted(() => ({ + getNodeByName: mockGetNodeByName, +})); + vi.mock('@/app/stores/workflowDocument.store', () => ({ - useWorkflowDocumentStore: vi.fn(() => ({ - getNodeByName: mockGetNodeByName, - })), + useWorkflowDocumentStore: vi.fn(() => mockDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), + injectWorkflowDocumentStore: () => ({ value: mockDocumentStore }), })); describe('schemaPreview.store', () => { @@ -116,15 +113,14 @@ describe('schemaPreview.store', () => { it('should track both the preview schema and the output one', async () => { const store = useSchemaPreviewStore(); - mockGetNodeByName.mockReturnValueOnce( + await store.trackSchemaPreviewExecution( + '123', mock({ id: 'test-node-id', type: options.nodeType, typeVersion: options.version, parameters: { resource: options.resource, operation: options.operation }, }), - ); - await store.trackSchemaPreviewExecution( mock>({ nodeName: 'Test', data: { @@ -147,25 +143,11 @@ describe('schemaPreview.store', () => { }); }); - it('should not track nodes without a schema preview', async () => { - const store = useSchemaPreviewStore(); - mockGetNodeByName.mockReturnValueOnce(mock()); - await store.trackSchemaPreviewExecution( - mock>({ - nodeName: 'Test', - data: { - executionStatus: 'success', - data: { main: [[{ json: { foo: 'bar', quz: 'qux' } }]] }, - }, - }), - ); - - expect(useTelemetry().track).not.toHaveBeenCalled(); - }); - it('should not track failed executions', async () => { const store = useSchemaPreviewStore(); await store.trackSchemaPreviewExecution( + '123', + mock({}), mock>({ data: { executionStatus: 'error', diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.ts b/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.ts index b19923bb5bd..df658939eba 100644 --- a/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.ts +++ b/packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.ts @@ -1,16 +1,11 @@ import * as schemaPreviewApi from './schemaPreview.api'; -import { createResultError, createResultOk, type Result } from 'n8n-workflow'; +import { createResultError, createResultOk, type INode, type Result } from 'n8n-workflow'; import { defineStore } from 'pinia'; -import { computed, reactive } from 'vue'; +import { reactive } from 'vue'; import { useRootStore } from '@n8n/stores/useRootStore'; import type { JSONSchema7 } from 'json-schema'; import type { PushPayload } from '@n8n/api-types'; import { useTelemetry } from '@/app/composables/useTelemetry'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; import { generateJsonSchema } from '@/app/utils/json-schema'; export const useSchemaPreviewStore = defineStore('schemaPreview', () => { @@ -22,10 +17,6 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => { const rootStore = useRootStore(); const telemetry = useTelemetry(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); function getSchemaPreviewKey({ nodeType, @@ -55,15 +46,11 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => { } } - async function trackSchemaPreviewExecution(pushEvent: PushPayload<'nodeExecuteAfterData'>) { - if (schemaPreviews.size === 0 || pushEvent.data.executionStatus !== 'success') { - return; - } - - const node = workflowDocumentStore.value.getNodeByName(pushEvent.nodeName) ?? null; - - if (!node) return; - + async function trackSchemaPreviewExecution( + workflowId: string, + node: INode, + pushEvent: PushPayload<'nodeExecuteAfterData'>, + ) { const { id, type, @@ -84,7 +71,7 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => { node_operation: operation, schema_preview: JSON.stringify(result.result), output_schema: JSON.stringify(generateJsonSchema(pushEvent.data.data?.main?.[0]?.[0]?.json)), - workflow_id: workflowsStore.workflowId, + workflow_id: workflowId, }); } diff --git a/packages/frontend/editor-ui/src/features/ndv/settings/composables/useNodeSettingsParameters.ts b/packages/frontend/editor-ui/src/features/ndv/settings/composables/useNodeSettingsParameters.ts index 041ca675525..3b2805989de 100644 --- a/packages/frontend/editor-ui/src/features/ndv/settings/composables/useNodeSettingsParameters.ts +++ b/packages/frontend/editor-ui/src/features/ndv/settings/composables/useNodeSettingsParameters.ts @@ -1,6 +1,6 @@ import get from 'lodash/get'; import set from 'lodash/set'; -import { computed, type Ref } from 'vue'; +import { type Ref } from 'vue'; import { type INode, type INodeParameters, @@ -24,17 +24,13 @@ import { import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useFocusPanelStore } from '@/app/stores/focusPanel.store'; import { useNDVStore } from '@/features/ndv/shared/ndv.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { CHAT_TRIGGER_NODE_TYPE, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/app/constants'; import { getMainAuthField, getNodeAuthFields, isAuthRelatedParameter, } from '@/app/utils/nodeTypesUtils'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useSettingsStore } from '@/app/stores/settings.store'; const hasPublicDisplayCondition = (parameter: INodeProperties, value: boolean) => @@ -58,10 +54,7 @@ const stripPublicDisplayCondition = (parameter: INodeProperties): INodePropertie }; export function useNodeSettingsParameters() { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeTypesStore = useNodeTypesStore(); const settingsStore = useSettingsStore(); const telemetry = useTelemetry(); diff --git a/packages/frontend/editor-ui/src/features/settings/communityNodes/composables/useInstallNode.ts b/packages/frontend/editor-ui/src/features/settings/communityNodes/composables/useInstallNode.ts index 00a950efcda..a88fa4f160a 100644 --- a/packages/frontend/editor-ui/src/features/settings/communityNodes/composables/useInstallNode.ts +++ b/packages/frontend/editor-ui/src/features/settings/communityNodes/composables/useInstallNode.ts @@ -2,14 +2,10 @@ import { useCommunityNodesStore } from '../communityNodes.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUsersStore } from '@/features/settings/users/users.store'; -import { computed, nextTick, ref } from 'vue'; +import { nextTick, ref } from 'vue'; import { i18n } from '@n8n/i18n'; import { useToast } from '@/app/composables/useToast'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils'; import { useTelemetry } from '@/app/composables/useTelemetry'; @@ -43,10 +39,7 @@ export function useInstallNode() { const communityNodesStore = useCommunityNodesStore(); const nodeTypesStore = useNodeTypesStore(); const credentialsStore = useCredentialsStore(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const userStore = useUsersStore(); const loading = ref(false); const toast = useToast(); diff --git a/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.vue b/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.vue index 402b765d68e..8d76c33d8ad 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.vue +++ b/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.vue @@ -16,12 +16,8 @@ import type { INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Int import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { isHttpRequestNodeType } from '@/features/setupPanel/setupPanel.utils'; -import { injectNDVStore } from '@/features/ndv/shared/ndv.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { useNDVStore } from '@/features/ndv/shared/ndv.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const NESTED_PARAM_TYPES = new Set([ 'collection', @@ -55,11 +51,8 @@ const emit = defineEmits<{ const i18n = useI18n(); const nodeTypesStore = useNodeTypesStore(); const nodeHelpers = useNodeHelpers(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); -const ndvStore = injectNDVStore(); +const workflowDocumentStore = injectWorkflowDocumentStore(); +const ndvStore = useNDVStore(); const nodeType = computed(() => nodeTypesStore.getNodeType(props.state.node.type, props.state.node.typeVersion), diff --git a/packages/frontend/editor-ui/src/features/setupPanel/composables/useTriggerExecution.ts b/packages/frontend/editor-ui/src/features/setupPanel/composables/useTriggerExecution.ts index 180a8d97bf2..680b05bba3d 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/composables/useTriggerExecution.ts +++ b/packages/frontend/editor-ui/src/features/setupPanel/composables/useTriggerExecution.ts @@ -8,10 +8,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { getTriggerNodeServiceName } from '@/app/utils/nodeTypesUtils'; import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes'; import { useLogsStore } from '@/app/stores/logs.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; /** * Wraps `useNodeExecution` with listening-hint logic for setup-panel cards. @@ -25,9 +22,7 @@ export function useTriggerExecution( const i18n = useI18n(); const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const logsStore = useLogsStore(); const { diff --git a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.nodeGrouping.test.ts b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.nodeGrouping.test.ts index 29fccc6570d..ca0a56f6bfd 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.nodeGrouping.test.ts +++ b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.nodeGrouping.test.ts @@ -38,7 +38,8 @@ const mockWorkflowDocumentStore = { name: '', settings: {}, getPinDataSnapshot: vi.fn().mockReturnValue({}), - connectionsByDestinationNode: {}, + connectionsBySourceNode: {} as Record, + connectionsByDestinationNode: {} as Record, workflowTriggerNodes: [] as INodeUi[], }; @@ -48,6 +49,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => { ...actual, useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), + injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }), }; }); @@ -126,6 +128,8 @@ describe('useWorkflowSetupState – node grouping', () => { mockGetNodeTypeDisplayableCredentials.mockReturnValue([]); mockWorkflowDocumentStore.allNodes = []; + mockWorkflowDocumentStore.connectionsBySourceNode = {}; + mockWorkflowDocumentStore.connectionsByDestinationNode = {}; mockWorkflowDocumentStore.getNodeByName = vi.fn(); mockWorkflowDocumentStore.getNodes = vi.fn(); mockUpdateNodeProperties.mockReset(); diff --git a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.test.ts b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.test.ts index 2abe70d3e24..8df783a8950 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.test.ts +++ b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.test.ts @@ -1,4 +1,4 @@ -import { ref, nextTick } from 'vue'; +import { ref, shallowRef, nextTick } from 'vue'; import { createTestingPinia } from '@pinia/testing'; import { createTestNode } from '@/__tests__/mocks'; @@ -55,6 +55,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => { return { ...actual, useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), }; }); diff --git a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.ts b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.ts index eb2275ca685..fa912e61cba 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.ts +++ b/packages/frontend/editor-ui/src/features/setupPanel/composables/useWorkflowSetupState.ts @@ -17,10 +17,7 @@ import { import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { getNodeCredentialTypes, @@ -63,9 +60,7 @@ export const useWorkflowSetupState = ( const nodeHelpers = useNodeHelpers(); const environmentsStore = useEnvironmentsStore(); const templatesStore = useTemplatesStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const sourceNodes = computed(() => nodes?.value ?? workflowDocumentStore.value.allNodes); diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts index 818d61162f8..1d1735b8fdd 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts @@ -10,10 +10,7 @@ import { type CommandBarItem } from '@n8n/design-system/components/N8nCommandBar import type { CommandGroup } from '../types'; import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store'; import { getResourcePermissions } from '@n8n/permissions'; import NodeIcon from '@/app/components/NodeIcon.vue'; @@ -41,9 +38,7 @@ export function useNodeCommands(options: { const collaborationStore = useCollaborationStore(); const { generateMergedNodesAndActions } = useActionsGenerator(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const isReadOnly = computed( () => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly, diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useRecentResources.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useRecentResources.ts index 1ecd05b72dd..4c5b00aa86e 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useRecentResources.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useRecentResources.ts @@ -5,16 +5,12 @@ import { useI18n } from '@n8n/i18n'; import { useRouter } from 'vue-router'; import { useLocalStorage } from '@vueuse/core'; import { VIEWS } from '@/app/constants'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { N8nIcon } from '@n8n/design-system'; import NodeIcon from '@/app/components/NodeIcon.vue'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const MAX_RECENT_ITEMS = 5; const MAX_RECENT_WORKFLOWS_TO_DISPLAY = 3; @@ -36,10 +32,7 @@ type RecentNodesMap = Record; export function useRecentResources() { const i18n = useI18n(); const router = useRouter(); - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowsListStore = useWorkflowsListStore(); const nodeTypesStore = useNodeTypesStore(); const { setNodeActive } = useCanvasOperations(); diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts index 41c59a9a69b..930d404c586 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts @@ -22,10 +22,7 @@ import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; import type { IWorkflowToShare } from '@/Interface'; import { saveAs } from 'file-saver'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import type { CommandGroup, CommandBarItem } from '../types'; import uniqBy from 'lodash/uniqBy'; import { nodeViewEventBus } from '@/app/event-bus'; @@ -60,9 +57,7 @@ export function useWorkflowCommands(): CommandGroup { const sourceControlStore = useSourceControlStore(); const collaborationStore = useCollaborationStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const router = useRouter(); diff --git a/packages/frontend/editor-ui/src/features/shared/editors/components/CodeNodeEditor/AskAI/AskAI.vue b/packages/frontend/editor-ui/src/features/shared/editors/components/CodeNodeEditor/AskAI/AskAI.vue index 4cbf1fd5dd9..2919958ee1f 100644 --- a/packages/frontend/editor-ui/src/features/shared/editors/components/CodeNodeEditor/AskAI/AskAI.vue +++ b/packages/frontend/editor-ui/src/features/shared/editors/components/CodeNodeEditor/AskAI/AskAI.vue @@ -25,10 +25,7 @@ import { ASK_AI_LOADING_DURATION_MS, } from '@/app/constants'; import type { AskAiRequest } from '@/features/ai/assistant/assistant.types'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const emit = defineEmits<{ submit: [code: string]; replaceCode: [code: string]; @@ -49,6 +46,7 @@ const props = withDefaults( const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); const i18n = useI18n(); const ndvStore = injectNDVStore(); +const workflowDocumentStore = injectWorkflowDocumentStore(); const loadingPhraseIndex = ref(0); const loaderProgress = ref(0); @@ -97,14 +95,12 @@ function getParentNodes() { if (!activeNode || !workflowId) return []; - const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)); - - return workflowDocumentStore + return workflowDocumentStore.value .getParentNodesByDepth(activeNode?.name) .filter(({ name }, i, nodes) => { return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; }) - .map((n) => workflowDocumentStore.getNodeByName(n.name)) + .map((n) => workflowDocumentStore.value.getNodeByName(n.name)) .filter((n) => n !== null); } diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.test.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.test.ts index 045f0f7e5ab..c7dc5033c13 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.test.ts @@ -19,19 +19,18 @@ import { import { CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow'; import type { INodeUi } from '@/Interface'; -let mockAllNodes: INodeUi[] = []; -let mockWorkflowTriggerNodes: INodeUi[] = []; +const mockDocumentStoreState = vi.hoisted(() => ({ + allNodes: [] as INodeUi[], + workflowTriggerNodes: [] as INodeUi[], + aiNodes: [] as INodeUi[], + name: '', + settings: {}, + getPinDataSnapshot: () => ({}), +})); vi.mock('@/app/stores/workflowDocument.store', () => ({ - useWorkflowDocumentStore: () => ({ - allNodes: mockAllNodes, - get workflowTriggerNodes() { - return mockWorkflowTriggerNodes; - }, - name: '', - settings: {}, - getPinDataSnapshot: () => ({}), - }), + useWorkflowDocumentStore: () => mockDocumentStoreState, createWorkflowDocumentId: (id: string) => `${id}@latest`, + injectWorkflowDocumentStore: () => ({ value: mockDocumentStoreState }), })); describe('useActions', () => { @@ -41,14 +40,14 @@ describe('useActions', () => { afterEach(() => { vi.clearAllMocks(); - mockAllNodes = []; + mockDocumentStoreState.allNodes = []; }); describe('getAddedNodesAndConnections', () => { test('should insert a manual trigger node when there are no triggers', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = []; + mockDocumentStoreState.workflowTriggerNodes = []; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -68,7 +67,7 @@ describe('useActions', () => { test('should not insert a manual trigger node when there is a trigger in the workflow', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -83,8 +82,8 @@ describe('useActions', () => { }); test('should insert a ChatTrigger node when an AI Agent is added on an empty canvas', () => { - mockWorkflowTriggerNodes = []; - mockAllNodes = []; + mockDocumentStoreState.workflowTriggerNodes = []; + mockDocumentStoreState.allNodes = []; const { getAddedNodesAndConnections } = useActions(); @@ -107,8 +106,8 @@ describe('useActions', () => { }); test('should insert a ChatTrigger node when an AI Agent is added with only a Manual Trigger present', () => { - mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; - mockAllNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as INodeUi]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.allNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as INodeUi]; const { getAddedNodesAndConnections } = useActions(); @@ -131,8 +130,8 @@ describe('useActions', () => { }); test('should not insert a ChatTrigger node when an AI Agent is added with a non-trigger node prseent', () => { - mockWorkflowTriggerNodes = [{ type: GITHUB_TRIGGER_NODE_TYPE } as never]; - mockAllNodes = [ + mockDocumentStoreState.workflowTriggerNodes = [{ type: GITHUB_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.allNodes = [ { type: GITHUB_TRIGGER_NODE_TYPE } as INodeUi, { type: HTTP_REQUEST_NODE_TYPE } as INodeUi, ]; @@ -146,8 +145,8 @@ describe('useActions', () => { }); test('should not insert a ChatTrigger node when an AI Agent is added with a Chat Trigger already present', () => { - mockWorkflowTriggerNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as never]; - mockAllNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as INodeUi]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.allNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as INodeUi]; const { getAddedNodesAndConnections } = useActions(); @@ -160,7 +159,7 @@ describe('useActions', () => { test('should insert a No Op node when a Loop Over Items Node is added', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = []; + mockDocumentStoreState.workflowTriggerNodes = []; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -192,7 +191,7 @@ describe('useActions', () => { const nodeCreatorStore = useNodeCreatorStore(); const nodeTypesStore = useNodeTypesStore(); - mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -229,7 +228,7 @@ describe('useActions', () => { const nodeCreatorStore = useNodeCreatorStore(); const nodeTypesStore = useNodeTypesStore(); - mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never]; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -335,7 +334,7 @@ describe('useActions', () => { test('should preserve actionName in nodes array', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = []; + mockDocumentStoreState.workflowTriggerNodes = []; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -359,7 +358,7 @@ describe('useActions', () => { test('should preserve actionName when no trigger is prepended', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); @@ -380,7 +379,7 @@ describe('useActions', () => { test('should work with multiple nodes having actionNames', () => { const nodeCreatorStore = useNodeCreatorStore(); - mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; + mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never]; vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, ); diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.ts index eef67e3a48a..dd37a7bf5e9 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useActions.ts @@ -38,11 +38,7 @@ import { import type { BaseTextKey } from '@n8n/i18n'; import type { Telemetry } from '@/app/plugins/telemetry'; import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; @@ -53,14 +49,10 @@ import { } from '../nodeCreator.utils'; import { useI18n } from '@n8n/i18n'; import { PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils'; -import { useCanvasStore } from '@/app/stores/canvas.store'; import { CHANGE_ACTION } from '@/app/stores/workflowDocument/types'; export const useActions = () => { - const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const nodeCreatorStore = useNodeCreatorStore(); const nodeTypesStore = useNodeTypesStore(); const i18n = useI18n(); @@ -258,10 +250,8 @@ export const useActions = () => { function shouldPrependManualTrigger(addedNodes: AddedNode[]): boolean { const { selectedView, openSource } = useNodeCreatorStore(); - const { workflowId } = useWorkflowsStore(); - const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)); const hasTrigger = addedNodes.some((node) => useNodeTypesStore().isTriggerNode(node.type)); - const workflowContainsTrigger = workflowDocumentStore.workflowTriggerNodes.length > 0; + const workflowContainsTrigger = workflowDocumentStore.value.workflowTriggerNodes.length > 0; const isTriggerPanel = selectedView === TRIGGER_NODE_CREATOR_VIEW; const onlyStickyNodes = addedNodes.every((node) => node.type === STICKY_NODE_TYPE); @@ -294,7 +284,7 @@ export const useActions = () => { // AI-226: Prepend LLM Chain node when adding a language model function shouldPrependLLMChain(addedNodes: AddedNode[]): boolean { - const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0; if (canvasHasAINodes) return false; return addedNodes.some((node) => { diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.test.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.test.ts index c3805f380b2..49bbad39d0a 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.test.ts @@ -1,3 +1,4 @@ +import { shallowRef } from 'vue'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -25,6 +26,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => { return { ...actual, useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), + injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore), createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'), }; }); diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.ts index 892dd241ee9..78025d44b49 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useGetNodeCreatorFilter.ts @@ -4,19 +4,11 @@ import type { NodeConnectionType, INodeInputConfiguration } from 'n8n-workflow'; import { NodeHelpers, NodeConnectionTypes, isHitlToolType } from 'n8n-workflow'; import type { NodeCreatorFilter } from './useViewStacks'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { computed } from 'vue'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export function useGetNodeCreatorFilter() { - const workflowStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); function getNodeCreatorFilter( nodeName: string, diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useViewStacks.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useViewStacks.ts index 6921e9598cd..cde8aa5714d 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useViewStacks.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/composables/useViewStacks.ts @@ -50,7 +50,6 @@ import { useKeyboardNavigation } from './useKeyboardNavigation'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { AI_TRANSFORM_NODE_TYPE, NodeConnectionTypes } from 'n8n-workflow'; import type { NodeConnectionType, INodeFilter } from 'n8n-workflow'; -import { useCanvasStore } from '@/app/stores/canvas.store'; import { useSettingsStore } from '@/app/stores/settings.store'; export type CommunityNodeDetails = { @@ -69,6 +68,7 @@ import { type NodeIconSource } from '@/app/utils/nodeIcon'; import { getThemedValue } from '@/app/utils/nodeTypesUtils'; import nodePopularity from 'virtual:node-popularity-data'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; export type NodeCreatorFilter = INodeFilter & { conditions?: Array<(item: INodeCreateElement) => boolean>; @@ -110,6 +110,7 @@ const nodePopularityMap = Object.values(nodePopularity).reduce((acc, node) => { export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { const nodeCreatorStore = useNodeCreatorStore(); + const workflowDocumentStore = injectWorkflowDocumentStore(); const { getActiveItemIndex } = useKeyboardNavigation(); const i18n = useI18n(); const settingsStore = useSettingsStore(); @@ -124,7 +125,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { if (stack.search && searchBaseItems.value) { let searchBase: INodeCreateElement[] = searchBaseItems.value; - const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0; if (searchBaseItems.value.length === 0) { searchBase = flattenCreateElements(stack.baselineItems ?? []); } @@ -281,7 +282,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { sortAlphabetically: boolean, ) { const aiNodes = items.filter((node): node is NodeCreateElement => isAINode(node)); - const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0; const isVectorStoresCategory = stack?.title === AI_CATEGORY_VECTOR_STORES; const isToolsCategory = stack?.title === AI_CATEGORY_TOOLS; if ( diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.store.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.store.ts index 2d8279f0e4d..685dc9b023b 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.store.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.store.ts @@ -23,10 +23,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import type { TelemetryNdvType } from '@/app/types/telemetry'; import { getNodeIconSource } from '@/app/utils/nodeIcon'; import { isVueFlowConnection } from '@/app/utils/typeGuards'; @@ -48,9 +45,7 @@ import { prepareCommunityNodeDetailsViewStack, transformNodeType } from './nodeC export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { const workflowsStore = useWorkflowsStore(); - const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), - ); + const workflowDocumentStore = injectWorkflowDocumentStore(); const ndvStore = useNDVStore(); const uiStore = useUIStore(); const nodeTypesStore = useNodeTypesStore(); diff --git a/packages/frontend/editor-ui/src/features/shared/tags/tags.store.ts b/packages/frontend/editor-ui/src/features/shared/tags/tags.store.ts index 9f54477c275..9edfd3d99e6 100644 --- a/packages/frontend/editor-ui/src/features/shared/tags/tags.store.ts +++ b/packages/frontend/editor-ui/src/features/shared/tags/tags.store.ts @@ -5,11 +5,7 @@ import { defineStore } from 'pinia'; import { useRootStore } from '@n8n/stores/useRootStore'; import { computed, ref } from 'vue'; import { hasPermission } from '@/app/utils/rbac/permissions'; -import { - useWorkflowDocumentStore, - createWorkflowDocumentId, -} from '@/app/stores/workflowDocument.store'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const apiMapping = { [STORES.TAGS]: createTagsApi('/tags'), @@ -28,7 +24,7 @@ const createTagsStore = (id: typeof STORES.TAGS | typeof STORES.ANNOTATION_TAGS) const fetchedUsageCount = ref(false); const rootStore = useRootStore(); - const workflowsStore = useWorkflowsStore(); + const workflowDocumentStore = injectWorkflowDocumentStore(); // Computed @@ -127,13 +123,7 @@ const createTagsStore = (id: typeof STORES.TAGS | typeof STORES.ANNOTATION_TAGS) if (deleted) { deleteTag(id); - - // Update workflowDocumentStore (source of truth) if a workflow is active - if (workflowsStore.workflowId) { - const workflowDocumentId = createWorkflowDocumentId(workflowsStore.workflowId); - const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId); - workflowDocumentStore.removeTag(id); - } + workflowDocumentStore.value.removeTag(id); } return deleted; diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/WorkflowCanvas.vue b/packages/frontend/editor-ui/src/features/workflows/canvas/components/WorkflowCanvas.vue index 9c343c1ec4a..097050fa8ab 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/WorkflowCanvas.vue +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/WorkflowCanvas.vue @@ -10,11 +10,7 @@ import { computed, ref, useCssModule, useTemplateRef } from 'vue'; import type { CanvasEventBusEvents } from '../canvas.types'; import { useCanvasMapping } from '../composables/useCanvasMapping'; import Canvas from './Canvas.vue'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowDocumentId, - useWorkflowDocumentStore, -} from '@/app/stores/workflowDocument.store'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; defineOptions({ inheritAttrs: false, @@ -43,10 +39,7 @@ const props = withDefaults( const canvasRef = useTemplateRef('canvas'); const $style = useCssModule(); -const workflowsStore = useWorkflowsStore(); -const workflowDocumentStore = computed(() => - useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), -); +const workflowDocumentStore = injectWorkflowDocumentStore(); const { onNodesInitialized, viewport, viewportRef, getNodes, fitBounds } = useVueFlow(props.id); diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/buttons/CanvasControlButtons.vue b/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/buttons/CanvasControlButtons.vue index 942d7a10345..ca8c74d963f 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/buttons/CanvasControlButtons.vue +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/buttons/CanvasControlButtons.vue @@ -6,6 +6,7 @@ import { Controls } from '@vue-flow/controls'; import { computed } from 'vue'; import { useExperimentalNdvStore } from '../../../experimental/experimentalNdv.store'; import { N8nButton, N8nIconButton, N8nTooltip } from '@n8n/design-system'; +import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; const props = withDefaults( defineProps<{ zoom?: number; @@ -30,6 +31,8 @@ const i18n = useI18n(); const experimentalNdvStore = useExperimentalNdvStore(); +const workflowDocumentStore = injectWorkflowDocumentStore(); + const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(props.zoom)); const isToggleZoomVisible = computed(() => experimentalNdvStore.isZoomedViewEnabled); @@ -55,6 +58,10 @@ function onZoomToFit() { function onTidyUp() { emit('tidy-up'); } + +function handleClickCollapseAll() { + experimentalNdvStore.collapseAllNodes(workflowDocumentStore.value.allNodes); +}