diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts index 1cdb4f155dc..87bf6b47141 100644 --- a/packages/frontend/@n8n/stores/src/constants.ts +++ b/packages/frontend/@n8n/stores/src/constants.ts @@ -37,7 +37,6 @@ export const STORES = { FOLDERS: 'folders', MODULES: 'modules', FOCUS_PANEL: 'focusPanel', - WORKFLOW_STATE: 'workflowState', AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection', PERSONALIZED_TEMPLATES: 'personalizedTemplates', EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows', diff --git a/packages/frontend/editor-ui/MIGRATION_RECIPE.md b/packages/frontend/editor-ui/MIGRATION_RECIPE.md index a16ebc6bc06..5ef5d2ee61b 100644 --- a/packages/frontend/editor-ui/MIGRATION_RECIPE.md +++ b/packages/frontend/editor-ui/MIGRATION_RECIPE.md @@ -170,11 +170,35 @@ Object.defineProperty(workflowsStore, 'allNodes', { }); ``` +## Current workflow id (`workflowsStore.workflowId`) + +`workflowsStore.workflowId` is a deprecated global pointer to "the open workflow". +It is being removed: consumers become document-store-first and the id is derived +from the route. There is **no `storeToRefs` destructuring** of it — every access +is the literal `workflowsStore.workflowId` member expression, guarded by ESLint +(`no-restricted-syntax`, `warn` during migration, flipped to `error` once empty). + +The per-document store is keyed *by* the id, so it cannot itself answer "which +workflow is current" — that comes from the route (or the injected current-document +pointer). Pick the replacement by context: + +| Context | Get the current document | Read | +|---|---|---| +| Components / composables inside `WorkflowLayout` | `injectWorkflowDocumentStore()` (`ShallowRef`) | `workflowDocumentStore.value?.workflowId ?? ''` | +| Out-of-tree Pinia stores / standalone composables | `computed(() => useWorkflowDocumentStore(createWorkflowDocumentId(useWorkflowId().value)))` | `workflowDocumentStore.value.workflowId` | +| Non-reactive functions (push handlers) | receive `documentId` via `options`, resolved per event in `usePushConnection.processEvent` from the injected document store | `useWorkflowDocumentStore(documentId)` / `useWorkflowExecutionStateStore(documentId)`, and their `.workflowId` for equality guards | +| Lifecycle snapshot (e.g. collaboration) | capture the id into a local/ref at the start of the operation | the captured value | + +Notes: +- `useWorkflowId()` (`@/app/composables/useWorkflowId`) resolves `inject(WorkflowIdKey)` first, then the route — use it in any setup context. +- Replace `watch(() => workflowsStore.workflowId, …)` with `watch(workflowId, …)` where `workflowId = useWorkflowId()` (or the route-derived computed). +- Tests that set `workflowsStore.workflowId = 'x'` (or `workflow.id`) move to providing `WorkflowIdKey` / the route param. Component/composable tests already `provide` `WorkflowIdKey`; push-handler tests pass `documentId` in the handler `options` argument. + ## What NOT to migrate - **`workflowsStore.workflowObject`** (39 files) — provides indirect node access via `Workflow` class methods (`.getNode()`, `.nodes`, `.getParentNodes()`, etc.). This is intentionally NOT migrated until both nodes **and** connections move to `workflowDocumentStore`. No ESLint guard for this — it's accepted tech debt. - **Execution-related methods** (e.g., `renameNodeSelectedAndExecution`, `removeNodeExecutionDataById`) — these are not node document state -- **`workflowState.executingNode`** and other execution-state properties — these are not node document state +- **`workflowExecutionStateStore.executingNode`** and other execution-state properties — these are not node document state (they live in `useWorkflowExecutionStateStore`) ## Maintaining this recipe diff --git a/packages/frontend/editor-ui/eslint.config.mjs b/packages/frontend/editor-ui/eslint.config.mjs index b8a51d0b4fb..e7dc9b4df42 100644 --- a/packages/frontend/editor-ui/eslint.config.mjs +++ b/packages/frontend/editor-ui/eslint.config.mjs @@ -116,6 +116,17 @@ export default defineConfig( message: 'Use workflowDocumentStore.setLastNodeParameters() instead of workflowsStore.setLastNodeParameters()', }, + { + selector: "MemberExpression[property.name='workflowId'][object.name='workflowsStore']", + message: + 'Use the workflow document store instead of workflowsStore.workflowId: workflowDocumentStore.workflowId (components/composables via injectWorkflowDocumentStore(); stores via useWorkflowId()) or the documentId from the handler options in push handlers', + }, + { + selector: + "CallExpression[callee.property.name='setWorkflowId'][callee.object.name='workflowsStore']", + message: + 'Do not call workflowsStore.setWorkflowId() — the current workflow id is derived from the route (useWorkflowId())', + }, ], // TODO: Remove these 'n8n-local-rules/no-internal-package-import': 'warn', diff --git a/packages/frontend/editor-ui/src/app/components/FocusPanel.vue b/packages/frontend/editor-ui/src/app/components/FocusPanel.vue index f806bd9c237..2df9a439ae8 100644 --- a/packages/frontend/editor-ui/src/app/components/FocusPanel.vue +++ b/packages/frontend/editor-ui/src/app/components/FocusPanel.vue @@ -39,6 +39,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry'; import { computedAsync } from '@vueuse/core'; import { useExecutionData } from '@/features/execution/executions/composables/useExecutionData'; import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import ExperimentalNodeDetailsDrawer from '@/features/workflows/canvas/experimental/components/ExperimentalNodeDetailsDrawer.vue'; import { useExperimentalNdvStore } from '@/features/workflows/canvas/experimental/experimentalNdv.store'; import { injectNDVStore } from '@/features/ndv/shared/ndv.store'; @@ -51,7 +52,6 @@ import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store'; import { N8nIcon, N8nInfoTip, N8nInput, N8nRadioButtons, N8nText } from '@n8n/design-system'; import { useInjectWorkflowId } from '@/app/composables/useInjectWorkflowId'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; defineOptions({ name: 'FocusPanel' }); const props = defineProps<{ @@ -73,7 +73,9 @@ const nodeHelpers = useNodeHelpers(); const focusPanelStore = useFocusPanelStore(); const workflowId = useInjectWorkflowId(); const workflowDocumentStore = injectWorkflowDocumentStore(); -const workflowState = injectWorkflowState(); +const workflowExecutionStateStore = computed(() => + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), +); const nodeTypesStore = useNodeTypesStore(); const setupPanelStore = useSetupPanelStore(); const telemetry = useTelemetry(); @@ -225,7 +227,7 @@ const targetNodeParameterContext = computed - workflowState.executingNode.isNodeExecuting(node.value?.name ?? ''), + workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.value?.name ?? ''), ); const selectedNodeIds = computed(() => vueFlow.getSelectedNodes.value.map((n) => n.id)); diff --git a/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts b/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts index a053b7e4f3f..84adb139fce 100644 --- a/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts +++ b/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts @@ -34,11 +34,6 @@ import { usePinnedData } from '@/app/composables/usePinnedData'; import { useMessage } from '@/app/composables/useMessage'; import { useToast } from '@/app/composables/useToast'; import * as buttonParameterUtils from '@/features/ndv/parameters/utils/buttonParameter.utils'; -import { - injectWorkflowState, - useWorkflowState, - type WorkflowState, -} from '@/app/composables/useWorkflowState'; vi.mock('vue-router', () => ({ useRouter: () => ({}), @@ -105,14 +100,6 @@ vi.mock('@/app/composables/useMessage', () => { }; }); -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({ ...(await importOriginal()), injectWorkflowDocumentStore: vi.fn(), @@ -128,7 +115,7 @@ let runWorkflow: ReturnType; let externalHooks: ReturnType; let message: ReturnType; let toast: ReturnType; -let workflowState: WorkflowState; +let workflowExecutionStateStore: ReturnType; let nodeViewEventBusEmitSpy: ReturnType; describe('NodeExecuteButton', () => { @@ -148,8 +135,9 @@ describe('NodeExecuteButton', () => { workflowsStore.workflowId = 'abc123'; workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('abc123')); vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocumentStore)); - workflowState = useWorkflowState(); - vi.mocked(injectWorkflowState).mockReturnValue(workflowState); + workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('abc123'), + ); nodeTypesStore = mockedStore(useNodeTypesStore); ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId('abc123')); @@ -238,7 +226,7 @@ describe('NodeExecuteButton', () => { it('displays "Stop Listening" when node is running and is a trigger node', () => { const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE }); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); + workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; vi.spyOn( useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), @@ -253,7 +241,7 @@ describe('NodeExecuteButton', () => { it('sets button to loading state when node is executing', () => { const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE }); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); + workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); vi.spyOn( useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), 'isWorkflowRunning', @@ -288,7 +276,7 @@ describe('NodeExecuteButton', () => { 'isWorkflowRunning', 'get', ).mockReturnValue(true); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false); + workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue( mockNode({ name: 'test-node', type: SET_NODE_TYPE }), ); @@ -348,8 +336,8 @@ describe('NodeExecuteButton', () => { 'get', ).mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; - useWorkflowState().setActiveExecutionId('test-execution-id'); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); + workflowExecutionStateStore.setActiveExecutionId('test-execution-id'); + workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue( mockNode({ name: 'test-node', type: SET_NODE_TYPE }), ); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowCanvasHost.vue b/packages/frontend/editor-ui/src/app/components/WorkflowCanvasHost.vue index bddba1bfdc3..bd6eb387f63 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowCanvasHost.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowCanvasHost.vue @@ -1,12 +1,7 @@ diff --git a/packages/frontend/editor-ui/src/features/agents/components/WorkflowExecutionLogViewer.vue b/packages/frontend/editor-ui/src/features/agents/components/WorkflowExecutionLogViewer.vue index bed033c18a4..8f1817cba92 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/WorkflowExecutionLogViewer.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/WorkflowExecutionLogViewer.vue @@ -7,7 +7,8 @@ import type { Chat } from '@n8n/chat/types'; import { WorkflowIdKey } from '@/app/constants/injectionKeys'; import { useExecutionsStore } from '@/features/execution/executions/executions.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { useWorkflowState } from '@/app/composables/useWorkflowState'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import LogsOverviewRow from '@/features/execution/logs/components/LogsOverviewRow.vue'; @@ -30,7 +31,6 @@ const props = defineProps<{ const i18n = useI18n(); const executionsStore = useExecutionsStore(); const workflowsStore = useWorkflowsStore(); -const workflowState = useWorkflowState(); const workflowHelpers = useWorkflowHelpers(); const nodeTypesStore = useNodeTypesStore(); @@ -176,7 +176,9 @@ onMounted(async () => { // pairedItemMappings, and various NodeErrorView code paths read from the // store rather than the prop, so the prop alone isn't enough to make the // table/JSON views render correctly for non-trivial nodes. - workflowState.setWorkflowExecutionData(result); + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setWorkflowExecutionData(result); // Default-select the first entry (the trigger) so the user sees data immediately. const first = flatEntries.value[0]; if (first) selected.value = first; @@ -192,7 +194,9 @@ onMounted(async () => { onBeforeUnmount(() => { unmounted = true; // Restore whatever execution data was in the store before we hijacked it. - workflowState.setWorkflowExecutionData(previousWorkflowExecutionData); + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setWorkflowExecutionData(previousWorkflowExecutionData); }); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts index e0786bf2ae1..06637f0c155 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts @@ -23,11 +23,6 @@ import { DEFAULT_POSTHOG_SETTINGS } from '@/app/stores/posthog.store.test'; import { nextTick, reactive } from 'vue'; import * as chatAPI from '@/features/ai/assistant/assistant.api'; import * as telemetryModule from '@/app/composables/useTelemetry'; -import { - injectWorkflowState, - useWorkflowState, - type WorkflowState, -} from '@/app/composables/useWorkflowState'; import type { Telemetry } from '@/app/plugins/telemetry'; import type { ChatUI } from '@n8n/design-system/types/assistant'; import type { ChatRequest } from '@/features/ai/assistant/assistant.types'; @@ -69,15 +64,6 @@ vi.mock('@/app/composables/useToast', () => ({ }), })); -// Mock to inject workflowState -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - // Mock useWorkflowSaving const saveCurrentWorkflowMock = vi.fn().mockResolvedValue(true); vi.mock('@/app/composables/useWorkflowSaving', () => ({ @@ -148,8 +134,6 @@ vi.mock('vue-router', () => ({ RouterLink: vi.fn(), })); -let workflowState: WorkflowState; - describe('AI Builder store', () => { beforeEach(() => { mockDocumentState = undefined; @@ -182,9 +166,6 @@ describe('AI Builder store', () => { workflowDocumentStore.setConnections({}); workflowsStore.setWorkflowExecutionData(null); - workflowState = useWorkflowState(); - vi.mocked(injectWorkflowState).mockReturnValue(workflowState); - getNodeTypeSpy = vi.fn(); vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(getNodeTypeSpy); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AIBuilderDiffModal.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AIBuilderDiffModal.test.ts index 85a42a21369..c83af77b158 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AIBuilderDiffModal.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AIBuilderDiffModal.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { defineComponent, h, ref } from 'vue'; +import { defineComponent, h } from 'vue'; import { createComponentRenderer } from '@/__tests__/render'; import { createTestingPinia } from '@pinia/testing'; import { createEventBus } from '@n8n/utils/event-bus'; @@ -66,17 +66,6 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({ }), })); -// Mock useWorkflowState -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(() => ({ - isWorkflowRunning: ref(false), - })), - }; -}); - // Mock useWorkflowSaving vi.mock('@/app/composables/useWorkflowSaving', () => ({ useWorkflowSaving: () => ({ 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 5c5f2ba2d39..27761c32f7c 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 @@ -65,13 +65,6 @@ vi.mock('@/app/stores/ui.store', () => ({ }), })); -const mockUpdateNodeProperties = vi.fn(); -vi.mock('@/app/composables/useWorkflowState', () => ({ - injectWorkflowState: () => ({ - updateNodeProperties: mockUpdateNodeProperties, - }), -})); - vi.mock('@/app/composables/useNodeHelpers', () => ({ useNodeHelpers: () => ({ updateNodesParameterIssues: vi.fn(), diff --git a/packages/frontend/editor-ui/src/features/credentials/quickConnect/components/QuickConnectButton.test.ts b/packages/frontend/editor-ui/src/features/credentials/quickConnect/components/QuickConnectButton.test.ts index 3ae890f368e..ff0436a7912 100644 --- a/packages/frontend/editor-ui/src/features/credentials/quickConnect/components/QuickConnectButton.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/quickConnect/components/QuickConnectButton.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { screen } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { createTestingPinia } from '@pinia/testing'; @@ -9,10 +9,6 @@ import { useCredentialsStore } from '../../credentials.store'; import { mockedStore } from '@/__tests__/utils'; import type { ICredentialType } from 'n8n-workflow'; -vi.mock('@/app/composables/useWorkflowState', () => ({ - injectWorkflowState: vi.fn(), -})); - const googleSheetsOAuth2Api: ICredentialType = { name: 'googleSheetsOAuth2Api', extends: ['googleOAuth2Api'], 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 3ed64c4e694..f9be0889960 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 @@ -2,16 +2,12 @@ import { createTestingPinia } from '@pinia/testing'; import { mockedStore } from '@/__tests__/utils'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useUIStore } from '@/app/stores/ui.store'; -import { - injectWorkflowState, - useWorkflowState, - type WorkflowState, -} from '@/app/composables/useWorkflowState'; import { useExecutionDebugging } from './useExecutionDebugging'; import type { INodeUi } from '@/Interface'; import type { IExecutionResponse } from '../executions.types'; import { useToast } from '@/app/composables/useToast'; import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow'; vi.mock('@/app/composables/useToast', () => { @@ -23,16 +19,9 @@ vi.mock('@/app/composables/useToast', () => { }; }); -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ mockWorkflowDocumentStore: { + documentId: 'test-id@latest', allNodes: [] as INodeUi[], workflowTriggerNodes: [] as INodeUi[], getParentNodes: vi.fn().mockReturnValue([]), @@ -51,9 +40,9 @@ vi.mock('@/app/stores/workflowDocument.store', () => ({ injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }), })); -let workflowState: WorkflowState; let executionDebugging: ReturnType; let toast: ReturnType; +let executionStateStore: ReturnType; describe('useExecutionDebugging()', () => { beforeEach(() => { @@ -68,8 +57,9 @@ describe('useExecutionDebugging()', () => { toast = useToast(); - workflowState = useWorkflowState(); - vi.mocked(injectWorkflowState).mockReturnValue(workflowState); + // Production resolves the execution-state store by the injected document + // store's `documentId` ('test-id@latest' on the mock above). + executionStateStore = useWorkflowExecutionStateStore('test-id@latest'); executionDebugging = useExecutionDebugging(); }); @@ -192,7 +182,7 @@ describe('useExecutionDebugging()', () => { mockWorkflowDocumentStore.allNodes = [{ name: 'testNode2' }] as INodeUi[]; workflowStore.getExecution.mockResolvedValueOnce(mockExecution); - const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData'); + const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData'); await executionDebugging.applyExecutionData('1'); @@ -220,7 +210,7 @@ describe('useExecutionDebugging()', () => { mockWorkflowDocumentStore.allNodes = [{ name: 'testNode' }] as INodeUi[]; workflowStore.getExecution.mockResolvedValueOnce(mockExecution); - const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData'); + const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData'); await executionDebugging.applyExecutionData('1'); 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 65bc06bc4a2..cd658cfacd5 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 @@ -3,7 +3,6 @@ import { useRouter } from 'vue-router'; import { useI18n } from '@n8n/i18n'; import { useMessage } from '@/app/composables/useMessage'; import { useToast } from '@/app/composables/useToast'; -import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState'; import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/app/constants'; import { DEBUG_PAYWALL_MODAL_KEY } from '../executions.constants'; import type { INodeUi } from '@/Interface'; @@ -22,12 +21,7 @@ import { sanitizeHtml } from '@/app/utils/htmlUtils'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; import { isTrimmedNodeExecutionData } from 'n8n-workflow'; -/** - * @param providedWorkflowState - Optional workflow state to use instead of injecting. - * This is needed when called from the same component that provides WorkflowStateKey - * (e.g., WorkflowLayout), since Vue's provide/inject works parent-to-child only. - */ -export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) => { +export const useExecutionDebugging = () => { const telemetry = useTelemetry(); const router = useRouter(); @@ -36,7 +30,6 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) => const toast = useToast(); const workflowsStore = useWorkflowsStore(); const workflowDocumentStore = injectWorkflowDocumentStore(); - const workflowState = providedWorkflowState ?? injectWorkflowState(); const settingsStore = useSettingsStore(); const uiStore = useUIStore(); const { markStateDirty } = uiStore; @@ -106,7 +99,9 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) => // Set execution data workflowDocumentStore.value.resetAllNodesIssues(); - workflowState.setWorkflowExecutionData(execution); + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId).setWorkflowExecutionData( + execution, + ); // Pin data of all nodes which do not have a parent node const pinnableNodes = workflowNodes.filter( diff --git a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.test.ts b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.test.ts index 2df9d70399e..c84b6a6a1d5 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.test.ts @@ -11,6 +11,8 @@ import { useWorkflowDocumentStore, createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import type { IExecutionResponse } from '@/features/execution/executions/executions.types'; import { computed, h, nextTick, ref, shallowRef } from 'vue'; import { aiAgentNode, @@ -21,7 +23,7 @@ import { nodeTypes, } from '../__test__/data'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { IN_PROGRESS_EXECUTION_ID, WorkflowStateKey } from '@/app/constants'; +import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; @@ -36,7 +38,6 @@ import { userEvent } from '@testing-library/user-event'; import type { ChatMessage } from '@n8n/chat/types'; import * as useChatMessaging from '@/features/execution/logs/composables/useChatMessaging'; import { useToast } from '@/app/composables/useToast'; -import { useWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState'; import type { IWorkflowDb } from '@/Interface'; vi.mock('@/app/composables/useToast', () => { @@ -81,7 +82,6 @@ describe('LogsPanel', () => { let logsStore: ReturnType>; let ndvStore: ReturnType>; let uiStore: ReturnType>; - let workflowState: WorkflowState; let aiChatExecutionResponse: typeof aiChatExecutionResponseTemplate; @@ -92,6 +92,15 @@ describe('LogsPanel', () => { ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflow.id)); } + // Production stages execution data on the per-document execution-state store. + // The component resolves it from the injected document store, which is keyed by + // `workflowsStore.workflowId` — so resolve the same store here for test setup. + function setExecutionData(execution: IExecutionResponse | null) { + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setWorkflowExecutionData(execution); + } + function render() { const wfId = workflowsStore.workflowId; const wrapper = renderComponent(LogsPanel, { @@ -99,7 +108,6 @@ describe('LogsPanel', () => { provide: { [ChatSymbol as symbol]: {}, [ChatOptionsSymbol as symbol]: {}, - [WorkflowStateKey as symbol]: workflowState, [WorkflowIdKey as unknown as string]: computed(() => wfId), [WorkflowDocumentStoreKey as symbol]: shallowRef( useWorkflowDocumentStore(createWorkflowDocumentId(wfId)), @@ -128,8 +136,7 @@ describe('LogsPanel', () => { setActivePinia(pinia); workflowsStore = mockedStore(useWorkflowsStore); - workflowState = useWorkflowState(); - workflowState.setWorkflowExecutionData(null); + setExecutionData(null); logsStore = mockedStore(useLogsStore); logsStore.toggleOpen(false); @@ -190,7 +197,7 @@ describe('LogsPanel', () => { it('should render only output panel of selected node by default', async () => { logsStore.toggleOpen(true); setWorkflow(aiManualWorkflow); - workflowState.setWorkflowExecutionData(aiManualExecutionResponse); + setExecutionData(aiManualExecutionResponse); const rendered = render(); @@ -204,7 +211,7 @@ describe('LogsPanel', () => { it('should render both input and output panel of selected node by default if it is sub node', async () => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); @@ -247,7 +254,7 @@ describe('LogsPanel', () => { it('should open log details panel when a log entry is clicked in the logs overview panel', async () => { setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); @@ -264,7 +271,7 @@ describe('LogsPanel', () => { it("should show the button to toggle panel in the header of log details panel when it's opened", async () => { setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); @@ -329,7 +336,7 @@ describe('LogsPanel', () => { it('should reflect changes to execution data in workflow store if execution is in progress', async () => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData({ + setExecutionData({ ...aiChatExecutionResponse, id: IN_PROGRESS_EXECUTION_ID, status: 'running', @@ -388,7 +395,7 @@ describe('LogsPanel', () => { expect(await lastTreeItem.findByText('Success')).toBeInTheDocument(); expect(lastTreeItem.getByText('in 33ms')).toBeInTheDocument(); - workflowState.setWorkflowExecutionData({ + setExecutionData({ ...workflowsStore.workflowExecutionData!, id: '1234', status: 'success', @@ -409,7 +416,7 @@ describe('LogsPanel', () => { const workflow = deepCopy(aiChatWorkflow); setWorkflow(workflow); logsStore.toggleOpen(true); - workflowState.setWorkflowExecutionData({ + setExecutionData({ ...aiChatExecutionResponse, id: '2345', status: 'success', @@ -434,7 +441,7 @@ describe('LogsPanel', () => { it('should open NDV if the button is clicked', async () => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); const aiAgentRow = (await rendered.findAllByRole('treeitem'))[0]; @@ -453,7 +460,7 @@ describe('LogsPanel', () => { it('should toggle subtree when chevron icon button is pressed', async () => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); const overview = within(rendered.getByTestId('logs-overview')); @@ -480,7 +487,7 @@ describe('LogsPanel', () => { it('should toggle input and output panel when the button is clicked', async () => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); @@ -510,7 +517,7 @@ describe('LogsPanel', () => { const workflow = deepCopy(aiChatWorkflow); workflow.id = 'test-workflow-id'; setWorkflow(workflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); const rendered = render(); @@ -535,7 +542,7 @@ describe('LogsPanel', () => { beforeEach(() => { logsStore.toggleOpen(true); setWorkflow(aiChatWorkflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); }); it('should allow to select previous and next row via keyboard shortcut', async () => { @@ -594,7 +601,7 @@ describe('LogsPanel', () => { const workflow = deepCopy(aiChatWorkflow); workflow.id = 'test-workflow-id'; setWorkflow(workflow); - workflowState.setWorkflowExecutionData(aiChatExecutionResponse); + setExecutionData(aiChatExecutionResponse); logsStore.toggleLogSelectionSync(true); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.test.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.test.ts index 81107b20f89..0b35d4c52f4 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.test.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useChatState.test.ts @@ -35,12 +35,6 @@ vi.mock('@/app/composables/useWorkflowHelpers', async (importOriginal) => { }), }; }); -vi.mock('@/app/composables/useWorkflowState', () => ({ - injectWorkflowState: vi.fn(() => ({ - setWorkflowExecutionData: vi.fn(), - setActiveExecutionId: vi.fn(), - })), -})); vi.mock('@/app/composables/useNodeHelpers', () => ({ useNodeHelpers: vi.fn(() => ({ updateNodesExecutionIssues: vi.fn(), 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 6110dc8a7ea..0efcda45bec 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 @@ -18,7 +18,6 @@ import { usePushConnectionStore } from '@/app/stores/pushConnection.store'; import { restoreChatHistory } from '@/features/execution/logs/logs.utils'; import { type INode, type INodeParameters, NodeHelpers } from 'n8n-workflow'; import { isChatNode } from '@/app/utils/aiUtils'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { MessageComponentKey } from '@n8n/chat/constants/messageComponents'; @@ -49,7 +48,6 @@ export function useChatState( const workflowExecutionState = computed(() => useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), ); - const workflowState = injectWorkflowState(); const rootStore = useRootStore(); const logsStore = useLogsStore(); const router = useRouter(); @@ -199,8 +197,8 @@ export function useChatState( } // Clear any existing execution to allow fresh webhook registration - workflowState.setWorkflowExecutionData(null); - workflowState.setActiveExecutionId(undefined); + workflowExecutionState.value.setWorkflowExecutionData(null); + workflowExecutionState.value.setActiveExecutionId(undefined); // Use the useRunWorkflow composable to properly register the webhook // Only include destinationNode if set for partial execution support @@ -331,7 +329,7 @@ export function useChatState( ); function refreshSession() { - workflowState.setWorkflowExecutionData(null); + workflowExecutionState.value.setWorkflowExecutionData(null); nodeHelpers.updateNodesExecutionIssues(); logsStore.resetChatSessionId(); logsStore.resetMessages(); diff --git a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.test.ts b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.test.ts index 406ca977036..1a83c76acbe 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.test.ts +++ b/packages/frontend/editor-ui/src/features/execution/logs/composables/useLogsExecutionData.test.ts @@ -15,29 +15,17 @@ import { } from '@/__tests__/mocks'; import { createRunExecutionData, type IRunExecutionData } from 'n8n-workflow'; import { useToast } from '@/app/composables/useToast'; -import { - injectWorkflowState, - useWorkflowState, - type WorkflowState, -} from '@/app/composables/useWorkflowState'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { computed } from 'vue'; vi.mock('@/app/composables/useToast'); -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - -let workflowState: WorkflowState; - describe(useLogsExecutionData, () => { let workflowsStore: ReturnType>; let workflowsListStore: ReturnType>; let nodeTypeStore: ReturnType>; + let executionStateStore: ReturnType; beforeEach(() => { setActivePinia(createTestingPinia({ stubActions: false })); @@ -45,8 +33,9 @@ describe(useLogsExecutionData, () => { workflowsStore = mockedStore(useWorkflowsStore); workflowsListStore = mockedStore(useWorkflowsListStore); - workflowState = useWorkflowState(); - vi.mocked(injectWorkflowState).mockReturnValue(workflowState); + // The composable resolves the execution-state store via the injected + // document store (falls back to `workflowsStore.workflowId`, '' here). + executionStateStore = useWorkflowExecutionStateStore(createWorkflowDocumentId('')); nodeTypeStore = mockedStore(useNodeTypesStore); nodeTypeStore.setNodeTypes(nodeTypes); @@ -54,7 +43,7 @@ describe(useLogsExecutionData, () => { describe('isEnabled', () => { beforeEach(() => { - workflowState.setWorkflowExecutionData( + executionStateStore.setWorkflowExecutionData( createTestWorkflowExecutionResponse({ data: createRunExecutionData({ resultData: { runData: { n0: [createTestTaskData()] } } }), workflowData: createTestWorkflow({ nodes: [createTestNode({ name: 'n0' })] }), @@ -81,7 +70,7 @@ describe(useLogsExecutionData, () => { beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }); - workflowState.setWorkflowExecutionData( + executionStateStore.setWorkflowExecutionData( createTestWorkflowExecutionResponse({ id: 'e0', workflowData: createTestWorkflow({ 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 ed94760d6ab..3703894ca65 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 @@ -20,7 +20,6 @@ import { isChatNode } from '@/app/utils/aiUtils'; import { CHAT_TRIGGER_NODE_TYPE, LOGS_EXECUTION_DATA_THROTTLE_DURATION } from '@/app/constants'; import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store'; import { useThrottleFn } from '@vueuse/core'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import { useThrottleWithReactiveDelay } from '@n8n/composables/useThrottleWithReactiveDelay'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; @@ -37,7 +36,6 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); const workflowDocumentStore = injectWorkflowDocumentStore(); - const workflowState = injectWorkflowState(); const toast = useToast(); const state = ref< @@ -109,7 +107,9 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData function resetExecutionData() { state.value = undefined; - workflowState.setWorkflowExecutionData(null); + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId).setWorkflowExecutionData( + null, + ); nodeHelpers.updateNodesExecutionIssues(); // Clear partial execution destination to allow full workflow execution useWorkflowExecutionStateStore( diff --git a/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.test.ts b/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.test.ts index 3e157081759..80127e74020 100644 --- a/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.test.ts @@ -14,12 +14,13 @@ import { setActivePinia } from 'pinia'; import { computed, shallowRef } from 'vue'; import { WorkflowIdKey } from '@/app/constants/injectionKeys'; -import { useWorkflowState } from '@/app/composables/useWorkflowState'; +import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { injectWorkflowDocumentStore, useWorkflowDocumentStore, createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; vi.mock('@/app/stores/workflowDocument.store', async () => { const actual = await vi.importActual('@/app/stores/workflowDocument.store'); @@ -66,7 +67,7 @@ const render = (props: Partial = {}, pinData?: INodeExecutionData[], runD setActivePinia(pinia); const workflow = createTestWorkflow({ nodes, connections }); - const workflowState = useWorkflowState(); + const workflowsStore = useWorkflowsStore(); const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)); workflowDocumentStore.hydrate(workflow); @@ -78,7 +79,11 @@ const render = (props: Partial = {}, pinData?: INodeExecutionData[], runD } if (runData) { - workflowState.setWorkflowExecutionData({ + // The component reads run data via `workflowsStore.getWorkflowExecution`, which + // resolves through the execution-state store keyed by `workflowsStore.workflowId`. + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setWorkflowExecutionData({ id: '', workflowData: { id: '', diff --git a/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.vue b/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.vue index 2079613dc69..1c7543d2403 100644 --- a/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.vue +++ b/packages/frontend/editor-ui/src/features/ndv/panel/components/InputPanel.vue @@ -37,7 +37,6 @@ import { useRouter } from 'vue-router'; import { useRunWorkflow } from '@/app/composables/useRunWorkflow'; import { N8nIcon, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; type MappingMode = 'debugging' | 'mapping'; export type Props = { @@ -112,7 +111,6 @@ const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowExecutionStateStore = computed(() => useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), ); -const workflowState = injectWorkflowState(); const router = useRouter(); const { runWorkflow } = useRunWorkflow({ router }); const { canReveal, isDynamicCredentials, revealData } = useExecutionRedaction(); @@ -202,12 +200,12 @@ const isExecutingPrevious = computed(() => { return false; } const triggeredNode = workflowsStore.executedNode; - const executingNode = workflowState.executingNode.executingNode; + const executingNode = workflowExecutionStateStore.value.executingNode.executingNode; if ( activeNode.value && triggeredNode === activeNode.value.name && - workflowState.executingNode.isNodeExecuting(props.currentNodeName) + workflowExecutionStateStore.value.executingNode.isNodeExecuting(props.currentNodeName) ) { return true; } @@ -215,7 +213,8 @@ const isExecutingPrevious = computed(() => { if (executingNode.length || triggeredNode) { return !!parentNodes.value.find( (node) => - workflowState.executingNode.isNodeExecuting(node.name) || node.name === triggeredNode, + workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.name) || + node.name === triggeredNode, ); } return false; diff --git a/packages/frontend/editor-ui/src/features/ndv/panel/components/OutputPanel.vue b/packages/frontend/editor-ui/src/features/ndv/panel/components/OutputPanel.vue index 222259f9056..5cd94cc5628 100644 --- a/packages/frontend/editor-ui/src/features/ndv/panel/components/OutputPanel.vue +++ b/packages/frontend/editor-ui/src/features/ndv/panel/components/OutputPanel.vue @@ -24,7 +24,6 @@ import RedactedDataState from '@/features/ndv/panel/components/RedactedDataState import NodeExecuteButton from '@/app/components/NodeExecuteButton.vue'; import { N8nIcon, N8nRadioButtons, N8nSpinner, N8nText } from '@n8n/design-system'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import { useUIStore } from '@/app/stores/ui.store'; import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/app/constants'; import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; @@ -81,7 +80,6 @@ const workflowId = useInjectWorkflowId(); const ndvStore = injectNDVStore(); const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); -const workflowState = injectWorkflowState(); const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowExecutionStateStore = computed(() => useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), @@ -162,7 +160,7 @@ const isNodeRunning = computed(() => { return ( workflowRunning.value && !!node.value && - workflowState.executingNode.isNodeExecuting(node.value.name) + workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.value.name) ); }); diff --git a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.test.ts b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.test.ts index 61b775da440..e30b85ac33a 100644 --- a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.test.ts +++ b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.test.ts @@ -11,13 +11,13 @@ import { createComponentRenderer } from '@/__tests__/render'; import NodeSettings from './NodeSettings.vue'; import { useNDVStore } from '@/features/ndv/shared/ndv.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useWorkflowState } from '@/app/composables/useWorkflowState'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { createWorkflowDocumentId, injectWorkflowDocumentStore, useWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; vi.mock('@/app/stores/workflowDocument.store', async () => { const actual = await vi.importActual('@/app/stores/workflowDocument.store'); @@ -54,7 +54,6 @@ const renderNodeSettings = (runData?: IRunData) => { const workflow = createTestWorkflow({ nodes: [httpNode], connections: {} }); const workflowsStore = useWorkflowsStore(); - const workflowState = useWorkflowState(); const nodeTypesStore = useNodeTypesStore(); workflowsStore.setWorkflowId(workflow.id); const ndvStore = useNDVStore(createWorkflowDocumentId(workflow.id)); @@ -65,7 +64,7 @@ const renderNodeSettings = (runData?: IRunData) => { ndvStore.activeNodeName = httpNode.name; if (runData) { - workflowState.setWorkflowExecutionData({ + useWorkflowExecutionStateStore(createWorkflowDocumentId(workflow.id)).setWorkflowExecutionData({ id: 'exec-1', workflowData: { ...workflow, diff --git a/packages/frontend/editor-ui/src/features/settings/usage/components/CommunityPlusEnrollmentModal.test.ts b/packages/frontend/editor-ui/src/features/settings/usage/components/CommunityPlusEnrollmentModal.test.ts index 67daf78f090..fb77425c6aa 100644 --- a/packages/frontend/editor-ui/src/features/settings/usage/components/CommunityPlusEnrollmentModal.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/usage/components/CommunityPlusEnrollmentModal.test.ts @@ -33,14 +33,6 @@ vi.mock('@/app/composables/useTelemetry', () => { }; }); -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - const renderComponent = createComponentRenderer(CommunityPlusEnrollmentModal, { global: { stubs: { diff --git a/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.test.ts b/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.test.ts index f38095819df..b983c1c3032 100644 --- a/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.test.ts +++ b/packages/frontend/editor-ui/src/features/setupPanel/components/cards/SetupCardBody.test.ts @@ -32,16 +32,6 @@ vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', () => ({ }, })); -const { mockUpdateNodeProperties } = vi.hoisted(() => ({ - mockUpdateNodeProperties: vi.fn(), -})); - -vi.mock('@/app/composables/useWorkflowState', () => ({ - injectWorkflowState: vi.fn(() => ({ - updateNodeProperties: mockUpdateNodeProperties, - })), -})); - const renderComponent = createComponentRenderer(SetupCardBody); const NODE_PROPERTIES = [ @@ -82,7 +72,6 @@ describe('SetupCardBody', () => { let nodeTypesStore: ReturnType>; beforeEach(() => { - mockUpdateNodeProperties.mockClear(); createTestingPinia(); nodeTypesStore = mockedStore(useNodeTypesStore); nodeTypesStore.getNodeType = vi.fn().mockReturnValue({ properties: NODE_PROPERTIES }); diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.test.ts index 9093953de33..742af719f5f 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.test.ts +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.test.ts @@ -40,11 +40,6 @@ import { useRootStore } from '@n8n/stores/useRootStore'; import { createTestingPinia } from '@pinia/testing'; import { MarkerType } from '@vue-flow/core'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { - injectWorkflowState, - useWorkflowState, - type WorkflowState, -} from '@/app/composables/useWorkflowState'; vi.mock('@n8n/i18n', async (importOriginal) => ({ ...(await importOriginal()), @@ -100,16 +95,6 @@ function createRenderDataWithExecutionIssuesByNodeName( }); } -vi.mock('@/app/composables/useWorkflowState', async () => { - const actual = await vi.importActual('@/app/composables/useWorkflowState'); - return { - ...actual, - injectWorkflowState: vi.fn(), - }; -}); - -let workflowState: WorkflowState; - beforeEach(() => { const pinia = createTestingPinia({ stubActions: false, @@ -143,9 +128,6 @@ beforeEach(() => { }); setActivePinia(pinia); - workflowState = useWorkflowState(); - vi.mocked(injectWorkflowState).mockReturnValue(workflowState); - renderNodeInputsMap.clear(); renderNodeOutputsMap.clear(); @@ -174,6 +156,12 @@ function setWorkflowRunning(running: boolean) { vi.spyOn(workflowExecutionStateStore, 'isWorkflowRunning', 'get').mockReturnValue(running); } +function getExecutingNode() { + const workflowsStore = useWorkflowsStore(); + return useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowsStore.workflowId)) + .executingNode; +} + /** * Populate the render data maps directly for tests * that rely on per-node inputs/outputs from the render data. @@ -230,7 +218,7 @@ describe('useCanvasMapping', () => { nodes, connections, }); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false); + getExecutingNode().isNodeExecuting = vi.fn().mockReturnValue(false); const { nodes: mappedNodes } = useCanvasMapping({ nodes: ref(nodes), @@ -329,7 +317,7 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); + getExecutingNode().isNodeExecuting = vi.fn().mockReturnValue(true); const { nodes: mappedNodes } = useCanvasMapping({ nodes: ref(nodes), @@ -1789,8 +1777,8 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.executingNode = []; - workflowState.executingNode.lastAddedExecutingNode = node1.name; + getExecutingNode().executingNode = []; + getExecutingNode().lastAddedExecutingNode = node1.name; setWorkflowRunning(true); const { nodeExecutionWaitingForNextById } = useCanvasMapping({ @@ -1819,8 +1807,8 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.executingNode = []; - workflowState.executingNode.lastAddedExecutingNode = node1.name; + getExecutingNode().executingNode = []; + getExecutingNode().lastAddedExecutingNode = node1.name; setWorkflowRunning(false); const { nodeExecutionWaitingForNextById } = useCanvasMapping({ @@ -1849,8 +1837,8 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.executingNode = [node2.name]; - workflowState.executingNode.lastAddedExecutingNode = node1.name; + getExecutingNode().executingNode = [node2.name]; + getExecutingNode().lastAddedExecutingNode = node1.name; setWorkflowRunning(false); const { nodeExecutionWaitingForNextById } = useCanvasMapping({ @@ -2336,11 +2324,9 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.isNodeExecuting = vi - .fn() - .mockImplementation((nodeName: string) => { - return nodeName === manualTriggerNode.name; - }); + getExecutingNode().isNodeExecuting = vi.fn().mockImplementation((nodeName: string) => { + return nodeName === manualTriggerNode.name; + }); workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => { if (nodeName === manualTriggerNode.name) { @@ -2919,11 +2905,9 @@ describe('useCanvasMapping', () => { connections, }); - workflowState.executingNode.isNodeExecuting = vi - .fn() - .mockImplementation((nodeName: string) => { - return nodeName === manualTriggerNode.name; - }); + getExecutingNode().isNodeExecuting = vi.fn().mockImplementation((nodeName: string) => { + return nodeName === manualTriggerNode.name; + }); workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null); const { connections: mappedConnections } = useCanvasMapping({ diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts index 6ff2ddb5973..f8a9237c2f4 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts @@ -55,7 +55,6 @@ import { useNodeDirtiness } from '@/app/composables/useNodeDirtiness'; import { getNodeIconSource } from '@/app/utils/nodeIcon'; import * as workflowUtils from 'n8n-workflow/common'; import { throttledWatch } from '@vueuse/core'; -import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import type { WorkflowObjectAccessors } from '@/app/types'; export function useCanvasMapping({ @@ -75,7 +74,6 @@ export function useCanvasMapping({ const workflowExecutionStateStore = computed(() => useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), ); - const workflowState = injectWorkflowState(); const nodeTypesStore = useNodeTypesStore(); const nodeHelpers = useNodeHelpers(); const { dirtinessByName } = useNodeDirtiness(); @@ -249,7 +247,7 @@ export function useCanvasMapping({ const nodeExecutionRunningById = computed(() => nodes.value.reduce>((acc, node) => { - acc[node.id] = workflowState.executingNode.isNodeExecuting(node.name); + acc[node.id] = workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.name); return acc; }, {}), ); @@ -257,8 +255,8 @@ export function useCanvasMapping({ const nodeExecutionWaitingForNextById = computed(() => nodes.value.reduce>((acc, node) => { acc[node.id] = - node.name === workflowState.executingNode.lastAddedExecutingNode && - workflowState.executingNode.executingNode.length === 0 && + node.name === workflowExecutionStateStore.value.executingNode.lastAddedExecutingNode && + workflowExecutionStateStore.value.executingNode.executingNode.length === 0 && workflowExecutionStateStore.value.isWorkflowRunning; return acc;