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 edaf4fdc101..b21173e0f25 100644 --- a/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts +++ b/packages/frontend/editor-ui/src/app/components/FromAiParametersModal.test.ts @@ -21,6 +21,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({ allNodes: [] as Array<{ id: string; name: string; type: string }>, workflowTriggerNodes: [] as Array<{ id: string; name: string; type: string }>, name: '', + documentId: 'test-id', workflowId: 'test-workflow', settings: {}, getPinDataSnapshot: () => ({}), 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 c370b9913b5..a053b7e4f3f 100644 --- a/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts +++ b/packages/frontend/editor-ui/src/app/components/NodeExecuteButton.test.ts @@ -20,6 +20,7 @@ import { } from '@/app/constants'; import NodeExecuteButton from './NodeExecuteButton.vue'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { injectWorkflowDocumentStore, useWorkflowDocumentStore, @@ -223,7 +224,11 @@ describe('NodeExecuteButton', () => { it('displays "Stop Listening" when node is listening for events', () => { const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE }); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node); - workflowsStore.executionWaitingForWebhook = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'executionWaitingForWebhook', + 'get', + ).mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; const { getByRole } = renderComponent(); @@ -235,7 +240,11 @@ describe('NodeExecuteButton', () => { vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node); workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; - workflowsStore.isWorkflowRunning = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'isWorkflowRunning', + 'get', + ).mockReturnValue(true); const { getByRole } = renderComponent(); expect(getByRole('button').textContent).toBe('Stop Listening'); @@ -245,7 +254,11 @@ describe('NodeExecuteButton', () => { const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE }); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node); workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); - workflowsStore.isWorkflowRunning = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'isWorkflowRunning', + 'get', + ).mockReturnValue(true); const { getByRole } = renderComponent(); expect(getByRole('button')).toHaveAttribute('aria-busy', 'true'); @@ -270,7 +283,11 @@ describe('NodeExecuteButton', () => { }); it('should be disabled when workflow is running but node is not executing', async () => { - workflowsStore.isWorkflowRunning = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'isWorkflowRunning', + 'get', + ).mockReturnValue(true); workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false); vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue( mockNode({ name: 'test-node', type: SET_NODE_TYPE }), @@ -307,7 +324,11 @@ describe('NodeExecuteButton', () => { }); it('stops webhook when clicking button while listening for events', async () => { - workflowsStore.executionWaitingForWebhook = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'executionWaitingForWebhook', + 'get', + ).mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue( mockNode({ name: 'test-node', type: SET_NODE_TYPE }), @@ -321,7 +342,11 @@ describe('NodeExecuteButton', () => { }); it('stops execution when clicking button while workflow is running', async () => { - workflowsStore.isWorkflowRunning = true; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')), + 'isWorkflowRunning', + 'get', + ).mockReturnValue(true); nodeTypesStore.isTriggerNode = () => true; useWorkflowState().setActiveExecutionId('test-execution-id'); workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true); diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index 8abc8f6dabd..677008f5747 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -18,6 +18,7 @@ import type { ICredentialsResponse } from '@/features/credentials/credentials.ty import type { IWorkflowTemplate, IWorkflowTemplateNode } from '@n8n/rest-api-client/api/templates'; import { RemoveNodeCommand, ReplaceNodeParametersCommand } from '@/app/models/history'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useHistoryStore } from '@/app/stores/history.store'; import { getNDVStoreId, useNDVStore } from '@/features/ndv/shared/ndv.store'; @@ -4051,8 +4052,13 @@ describe('useCanvasOperations', () => { uiStore.resetLastInteractedWith = vi.fn(); executionsStore.activeExecution = null; - workflowsStore.executionWaitingForWebhook = true; workflowsStore.workflowId = 'workflow-id'; + const executionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('workflow-id'), + ); + // Spy on the getter — readonly wrapping prevents direct assignment, and + // createTestingPinia stubs setExecutionWaitingForWebhook so the action is a no-op. + vi.spyOn(executionStateStore, 'executionWaitingForWebhook', 'get').mockReturnValue(true); workflowsStore.lastSuccessfulExecution = {} as IExecutionResponse; workflowsStore.currentWorkflowExecutions = [ { @@ -4104,8 +4110,8 @@ describe('useCanvasOperations', () => { nodeCreatorStore.setNodeCreatorState = vi.fn(); workflowsStore.removeTestWebhook = vi.fn(); - - workflowsStore.executionWaitingForWebhook = false; + workflowsStore.workflowId = 'workflow-id'; + // Default state-store value is false; no override needed. const { resetWorkspace } = useCanvasOperations(); diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index bfc99e4ae29..55b311c766f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -56,6 +56,7 @@ import { useSettingsStore } from '@/app/stores/settings.store'; import { useTagsStore } from '@/features/shared/tags/tags.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import type { CanvasConnection, CanvasConnectionCreateData, @@ -139,6 +140,7 @@ import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store'; import { clearAllNodeResourceLocatorValues } from '@/features/workflows/templates/utils/templateTransforms'; import { useClipboard } from '@vueuse/core'; import { + createWorkflowDocumentId, pinDataToExecutionData, injectWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; @@ -2311,7 +2313,10 @@ export function useCanvasOperations() { }); // Make sure that if there is a waiting test-webhook, it gets removed - if (workflowsStore.executionWaitingForWebhook) { + const executionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + if (executionStateStore.executionWaitingForWebhook) { try { void workflowsStore.removeTestWebhook(workflowsStore.workflowId); } catch (error) {} 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 fd705620bdb..160b7f47e86 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.test.ts @@ -30,6 +30,7 @@ import { const { mockWorkflowsStore, + mockWorkflowExecutionStateStore, mockNodeTypesStore, mockNdvStore, mockRunWorkflow, @@ -39,14 +40,16 @@ const { mockNodeHelpers, } = vi.hoisted(() => ({ mockWorkflowsStore: { - isWorkflowRunning: false, executedNode: undefined as string | undefined, - executionWaitingForWebhook: false, workflowId: '123', - chatPartialExecutionDestinationNode: undefined as string | undefined, getNodeByName: vi.fn(), removeTestWebhook: vi.fn(), }, + mockWorkflowExecutionStateStore: { + isWorkflowRunning: false, + executionWaitingForWebhook: false, + chatPartialExecutionDestinationNode: undefined as string | undefined, + }, mockNodeTypesStore: { getNodeType: vi.fn(), isTriggerNode: vi.fn(), @@ -99,6 +102,10 @@ vi.mock('@/app/stores/workflows.store', () => ({ useWorkflowsStore: vi.fn().mockReturnValue(mockWorkflowsStore), })); +vi.mock('@/app/stores/workflowExecutionState.store', () => ({ + useWorkflowExecutionStateStore: vi.fn().mockReturnValue(mockWorkflowExecutionStateStore), +})); + vi.mock('@/app/stores/nodeTypes.store', () => ({ useNodeTypesStore: vi.fn().mockReturnValue(mockNodeTypesStore), })); @@ -199,10 +206,10 @@ describe('useNodeExecution', () => { uiStore = useUIStore(); // Reset store properties to defaults - mockWorkflowsStore.isWorkflowRunning = false; + mockWorkflowExecutionStateStore.isWorkflowRunning = false; mockWorkflowsStore.executedNode = undefined; - mockWorkflowsStore.executionWaitingForWebhook = false; - mockWorkflowsStore.chatPartialExecutionDestinationNode = undefined; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = false; + mockWorkflowExecutionStateStore.chatPartialExecutionDestinationNode = undefined; mockWorkflowDocumentStore.checkIfNodeHasChatParent.mockReturnValue(false); mockWorkflowsStore.removeTestWebhook.mockReset(); mockWorkflowsStore.getNodeByName.mockReset(); @@ -295,7 +302,7 @@ describe('useNodeExecution', () => { describe('isListening', () => { it('should return true when trigger node is waiting for webhook', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { isListening } = useNodeExecution(node); @@ -305,7 +312,7 @@ describe('useNodeExecution', () => { it('should return false when node is disabled', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: true })); const { isListening } = useNodeExecution(node); @@ -314,7 +321,7 @@ describe('useNodeExecution', () => { }); it('should return false when not a trigger node', () => { - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode()); const { isListening } = useNodeExecution(node); @@ -333,7 +340,7 @@ describe('useNodeExecution', () => { it('should return false when executed node is a different node', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; mockWorkflowsStore.executedNode = 'Other Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -350,7 +357,7 @@ describe('useNodeExecution', () => { name: WEBHOOK_NODE_TYPE, group: ['trigger'], } as INodeTypeDescription); - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -365,7 +372,7 @@ describe('useNodeExecution', () => { name: 'n8n-nodes-base.scheduleTrigger', group: ['schedule'], } as INodeTypeDescription); - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -380,7 +387,7 @@ describe('useNodeExecution', () => { name: MANUAL_TRIGGER_NODE_TYPE, group: ['trigger'], } as INodeTypeDescription); - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -392,7 +399,7 @@ describe('useNodeExecution', () => { describe('isExecuting', () => { it('should return true when node is running and not listening', () => { - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -413,7 +420,7 @@ describe('useNodeExecution', () => { describe('disabledReason', () => { it('should return empty string when listening', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { disabledReason } = useNodeExecution(node); @@ -443,7 +450,7 @@ describe('useNodeExecution', () => { }); it('should return workflow running message when another node is executing', () => { - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Other Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -464,7 +471,7 @@ describe('useNodeExecution', () => { describe('buttonLabel', () => { it('should return stopListening when isListening', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { buttonLabel } = useNodeExecution(node); @@ -558,7 +565,7 @@ describe('useNodeExecution', () => { it('should return undefined when listening', () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { buttonIcon } = useNodeExecution(node); @@ -694,7 +701,7 @@ describe('useNodeExecution', () => { it('should stop webhook when listening', async () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { execute } = useNodeExecution(node); @@ -710,7 +717,7 @@ describe('useNodeExecution', () => { name: WEBHOOK_NODE_TYPE, group: ['trigger'], } as INodeTypeDescription); - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); @@ -847,7 +854,7 @@ describe('useNodeExecution', () => { describe('stopExecution', () => { it('should stop webhook when listening', async () => { mockNodeTypesStore.isTriggerNode.mockReturnValue(true); - mockWorkflowsStore.executionWaitingForWebhook = true; + mockWorkflowExecutionStateStore.executionWaitingForWebhook = true; const node = ref(createTestNode({ disabled: false })); const { stopExecution } = useNodeExecution(node); @@ -862,7 +869,7 @@ describe('useNodeExecution', () => { name: WEBHOOK_NODE_TYPE, group: ['trigger'], } as INodeTypeDescription); - mockWorkflowsStore.isWorkflowRunning = true; + mockWorkflowExecutionStateStore.isWorkflowRunning = true; mockWorkflowsStore.executedNode = 'Test Node'; const node = ref(createTestNode({ name: 'Test Node' })); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts index a74555e27cc..81fabe32ee3 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeExecution.ts @@ -12,6 +12,7 @@ import { import type { INodeUi, IUpdateInformation } from '@/Interface'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { injectNDVStore } from '@/features/ndv/shared/ndv.store'; import { useUIStore } from '@/app/stores/ui.store'; @@ -101,6 +102,9 @@ export function useNodeExecution( const workflowState = injectWorkflowState(); const workflowDocumentStore = injectWorkflowDocumentStore(); + const workflowExecutionStateStore = computed(() => + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), + ); const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router }); const nodeHelpers = useNodeHelpers(); @@ -140,7 +144,8 @@ export function useNodeExecution( const isWebhookNode = computed(() => nodeType.value?.name === WEBHOOK_NODE_TYPE); const isNodeRunning = computed(() => { - if (!workflowsStore.isWorkflowRunning || codeGenerationInProgress.value) return false; + if (!workflowExecutionStateStore.value.isWorkflowRunning || codeGenerationInProgress.value) + return false; const triggeredNode = workflowsStore.executedNode; return ( workflowState.executingNode.isNodeExecuting(nodeRef.value?.name ?? '') || @@ -149,7 +154,7 @@ export function useNodeExecution( }); const isListening = computed(() => { - const waitingOnWebhook = workflowsStore.executionWaitingForWebhook; + const waitingOnWebhook = workflowExecutionStateStore.value.executionWaitingForWebhook; const executedNode = workflowsStore.executedNode; return ( @@ -199,7 +204,7 @@ export function useNodeExecution( return i18n.baseText('ndv.execute.requiredFieldsMissing'); } - if (workflowsStore.isWorkflowRunning && !isNodeRunning.value) { + if (workflowExecutionStateStore.value.isWorkflowRunning && !isNodeRunning.value) { return i18n.baseText('ndv.execute.workflowAlreadyRunning'); } diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.test.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.test.ts index 8ec26cb0bdf..e779bd247a2 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.test.ts @@ -17,6 +17,7 @@ import type { WorkflowState } from '@/app/composables/useWorkflowState'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useWorkflowDocumentStore, createWorkflowDocumentId, @@ -281,7 +282,12 @@ describe('executionFinished', () => { const workflowsListStore = useWorkflowsListStore(); const readyToRunStore = useReadyToRunStore(); - vi.spyOn(workflowsStore, 'activeExecutionId', 'get').mockReturnValue('123'); + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', name: 'Test Workflow', @@ -325,7 +331,12 @@ describe('executionFinished', () => { const workflowsListStore = useWorkflowsListStore(); const readyToRunStore = useReadyToRunStore(); - vi.spyOn(workflowsStore, 'activeExecutionId', 'get').mockReturnValue('123'); + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', name: 'Test Workflow', @@ -366,7 +377,12 @@ describe('executionFinished', () => { const workflowsListStore = useWorkflowsListStore(); const readyToRunStore = useReadyToRunStore(); - vi.spyOn(workflowsStore, 'activeExecutionId', 'get').mockReturnValue('123'); + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', name: 'Test Workflow', @@ -410,7 +426,12 @@ describe('executionFinished', () => { const workflowsListStore = useWorkflowsListStore(); const readyToRunStore = useReadyToRunStore(); - vi.spyOn(workflowsStore, 'activeExecutionId', 'get').mockReturnValue('123'); + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', name: 'Test Workflow', @@ -451,7 +472,12 @@ describe('executionFinished', () => { const workflowsListStore = useWorkflowsListStore(); const readyToRunStore = useReadyToRunStore(); - vi.spyOn(workflowsStore, 'activeExecutionId', 'get').mockReturnValue('123'); + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', name: 'Test Workflow', @@ -505,8 +531,13 @@ describe('executionFinished', () => { const workflowsListStore = mockedStore(useWorkflowsListStore); const uiStore = mockedStore(useUIStore); - // Set activeExecutionId directly on the store - workflowsStore.activeExecutionId = '123'; + // Set workflowId + activeExecutionId via the state store + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); // Mock getWorkflowById to return a workflow vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ @@ -567,7 +598,12 @@ describe('executionFinished', () => { const workflowsStore = mockedStore(useWorkflowsStore); const workflowsListStore = mockedStore(useWorkflowsListStore); - workflowsStore.activeExecutionId = '123'; + workflowsStore.workflowId = '1'; + vi.spyOn( + useWorkflowExecutionStateStore(createWorkflowDocumentId('1')), + 'activeExecutionId', + 'get', + ).mockReturnValue('123'); vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({ id: '1', @@ -615,8 +651,8 @@ describe('executionFinished', () => { setActivePinia(pinia); const workflowsStore = mockedStore(useWorkflowsStore); - // In iframe preview after resetWorkspace, activeExecutionId can be undefined - workflowsStore.activeExecutionId = undefined; + workflowsStore.workflowId = '1'; + // In iframe preview after resetWorkspace, activeExecutionId is undefined by default. const clearNodeExecutionQueue = vi.fn(); const workflowState = mock({ diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts index a73f7e1cb30..af307b4464a 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionFinished.ts @@ -18,10 +18,7 @@ import { useWorkflowDocumentStore, createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { useBuilderStore } from '@/features/ai/assistant/builder.store'; @@ -79,8 +76,12 @@ export async function executionFinished( options.workflowState.executingNode.lastAddedExecutingNode = null; options.workflowState.executingNode.clearNodeExecutionQueue(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + // No workflow is actively running, therefore we ignore this event - if (typeof workflowsStore.activeExecutionId === 'undefined') { + if (typeof workflowExecutionStateStore.activeExecutionId === 'undefined') { return; } @@ -259,9 +260,12 @@ export function getRunDataExecutedErrorMessage(execution: SimplifiedExecution) { return i18n.baseText('pushConnection.executionFailed.message'); } else if (execution.status === 'canceled') { const workflowsStore = useWorkflowsStore(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); return i18n.baseText('executionsList.showMessage.stopExecution.message', { - interpolate: { activeExecutionId: workflowsStore.activeExecutionId ?? '' }, + interpolate: { activeExecutionId: workflowExecutionStateStore.activeExecutionId ?? '' }, }); } @@ -480,8 +484,8 @@ export function setRunExecutionData( workflowState: WorkflowState, ) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), ); const nodeHelpers = useNodeHelpers(); const runDataExecutedErrorMessage = getRunDataExecutedErrorMessage(execution); @@ -502,7 +506,7 @@ export function setRunExecutionData( stoppedAt: execution.stoppedAt, }); executionDataStore.setExecutionRunData(runExecutionData); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId(undefined); // Set the node execution issues on all the nodes which produced an error so that // it can be displayed in the node-view @@ -519,7 +523,7 @@ export function setRunExecutionData( runExecutionData.resultData.runData[lastNodeExecuted][0].data?.main[0]?.length ?? 0; } - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId(undefined); void useExternalHooks().run('pushConnection.executionFinished', { itemsCount, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionRecovered.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionRecovered.ts index 68ec952cbeb..3263cd4c4e5 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionRecovered.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionRecovered.ts @@ -9,10 +9,8 @@ import { setRunExecutionData, } from './executionFinished'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import type { useRouter } from 'vue-router'; import type { WorkflowState } from '@/app/composables/useWorkflowState'; @@ -21,13 +19,13 @@ export async function executionRecovered( options: { router: ReturnType; workflowState: WorkflowState }, ) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), ); const uiStore = useUIStore(); // No workflow is actively running, therefore we ignore this event - if (typeof stateStore.activeExecutionId === 'undefined') { + if (typeof workflowExecutionStateStore.activeExecutionId === 'undefined') { return; } diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.test.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.test.ts index 45754a8ff5e..e7ce5364325 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.test.ts @@ -6,15 +6,12 @@ import { useWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; import type { ExecutionStarted } from '@n8n/api-types/push/execution'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; describe('executionStarted', () => { let workflowsStore: ReturnType; - let stateStore: ReturnType; + let workflowExecutionStateStore: ReturnType; function makeEvent(executionId = 'exec-1'): ExecutionStarted { return { @@ -32,15 +29,17 @@ describe('executionStarted', () => { const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('wf-123')); workflowDocumentStore.setName('My Workflow'); - stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-123')); + workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-123'), + ); }); it('should skip when activeExecutionId is undefined', async () => { // activeExecutionId defaults to undefined, no need to set it await executionStarted(makeEvent()); - // stateStore.activeExecutionId should remain undefined - expect(stateStore.activeExecutionId).toBeUndefined(); + // workflowExecutionStateStore.activeExecutionId should remain undefined + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); // No execution data store should have been created for exec-1 const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); @@ -48,11 +47,11 @@ describe('executionStarted', () => { }); it('should accept execution when activeExecutionId is null and populate workflowData from store', async () => { - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); await executionStarted(makeEvent('exec-1')); - expect(stateStore.activeExecutionId).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1'); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); expect(executionDataStore.execution).toMatchObject({ @@ -64,7 +63,7 @@ describe('executionStarted', () => { it('should not reinitialize when same execution ID arrives', async () => { // Set up an active execution with existing data - stateStore.promotePendingExecution('exec-1'); + workflowExecutionStateStore.promotePendingExecution('exec-1'); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution({ id: 'exec-1', @@ -81,8 +80,8 @@ describe('executionStarted', () => { await executionStarted(makeEvent('exec-1')); - // stateStore.activeExecutionId should remain 'exec-1' without change - expect(stateStore.activeExecutionId).toBe('exec-1'); + // workflowExecutionStateStore.activeExecutionId should remain 'exec-1' without change + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1'); // execution data should not have been overwritten (same reference or same id) expect(executionDataStore.execution?.id).toBe('exec-1'); @@ -114,7 +113,7 @@ describe('executionStarted', () => { // activeExecutionId defaults to undefined; in iframe context this should still accept await executionStarted(makeEvent('exec-2')); - expect(stateStore.activeExecutionId).toBe('exec-2'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-2'); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-2')); expect(executionDataStore.execution).toMatchObject({ @@ -125,7 +124,7 @@ describe('executionStarted', () => { it('should accept new execution and reset state when re-executing in iframe', async () => { // Set up an existing active execution - stateStore.promotePendingExecution('exec-1'); + workflowExecutionStateStore.promotePendingExecution('exec-1'); const oldExecStore = useExecutionDataStore(createExecutionDataId('exec-1')); oldExecStore.setExecution({ id: 'exec-1', @@ -142,7 +141,7 @@ describe('executionStarted', () => { await executionStarted(makeEvent('exec-2')); - expect(stateStore.activeExecutionId).toBe('exec-2'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-2'); const newExecStore = useExecutionDataStore(createExecutionDataId('exec-2')); expect(newExecStore.execution).toMatchObject({ @@ -153,7 +152,7 @@ describe('executionStarted', () => { it('should not reset when same execution ID arrives in iframe', async () => { // Set up an existing active execution with data - stateStore.promotePendingExecution('exec-1'); + workflowExecutionStateStore.promotePendingExecution('exec-1'); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution({ id: 'exec-1', @@ -169,7 +168,7 @@ describe('executionStarted', () => { await executionStarted(makeEvent('exec-1')); // Should remain exec-1 without reinitializing - expect(stateStore.activeExecutionId).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1'); }); }); }); diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts index 9c7f2133219..0fb8e8261cc 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/executionStarted.ts @@ -4,10 +4,7 @@ import { createWorkflowDocumentId, useWorkflowDocumentStore, } from '@/app/stores/workflowDocument.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { parse } from 'flatted'; import { createRunExecutionData } from 'n8n-workflow'; @@ -18,26 +15,29 @@ import type { IRunExecutionData } from 'n8n-workflow'; */ export async function executionStarted({ data }: ExecutionStarted) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + workflowDocumentStore.documentId, ); const isIframe = window !== window.parent; // In non-iframe context, undefined means "not tracking executions" → skip. // In iframe context, executionFinished resets activeExecutionId to undefined, // but we still want to accept new executions (re-execution scenario). - if (typeof stateStore.activeExecutionId === 'undefined' && !isIframe) { + if (typeof workflowExecutionStateStore.activeExecutionId === 'undefined' && !isIframe) { return; } // Determine if we need to (re)initialize execution tracking state const needsInit = - stateStore.activeExecutionId === null || - typeof stateStore.activeExecutionId === 'undefined' || - (isIframe && stateStore.activeExecutionId !== data.executionId); + workflowExecutionStateStore.activeExecutionId === null || + typeof workflowExecutionStateStore.activeExecutionId === 'undefined' || + (isIframe && workflowExecutionStateStore.activeExecutionId !== data.executionId); if (needsInit) { - stateStore.promotePendingExecution(data.executionId); + workflowExecutionStateStore.promotePendingExecution(data.executionId); } const executionDataStore = useExecutionDataStore(createExecutionDataId(data.executionId)); @@ -45,10 +45,6 @@ export async function executionStarted({ data }: ExecutionStarted) { // Initialize or reinitialize execution data to clear previous execution's // node status (e.g. DemoLayout iframe receiving push events for a new execution). if (!executionDataStore.execution?.data || needsInit) { - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowsStore.workflowId), - ); - executionDataStore.setExecution({ id: data.executionId, finished: false, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.test.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.test.ts index 90f28104a5b..157c2a461db 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.test.ts @@ -7,10 +7,8 @@ import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow'; import type { WorkflowState } from '@/app/composables/useWorkflowState'; import { mock } from 'vitest-mock-extended'; import type { Mocked } from 'vitest'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { createTestWorkflow, createTestWorkflowExecutionResponse } from '@/__tests__/mocks'; @@ -31,7 +29,7 @@ import { openFormPopupWindow } from '@/features/execution/executions/executions. describe('nodeExecuteAfter', () => { let mockOptions: { workflowState: Mocked }; let workflowsStore: ReturnType; - let stateStore: ReturnType; + let workflowExecutionStateStore: ReturnType; let executionDataStore: ReturnType; beforeEach(() => { @@ -41,7 +39,9 @@ describe('nodeExecuteAfter', () => { workflowsStore = useWorkflowsStore(); workflowsStore.setWorkflowId('test-wf'); - stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('test-wf')); + workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('test-wf'), + ); executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution( @@ -53,7 +53,7 @@ describe('nodeExecuteAfter', () => { }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); mockOptions = { workflowState: mock({ diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.ts index 0a6914b9fca..eef33feb4ec 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfter.ts @@ -1,10 +1,8 @@ import type { NodeExecuteAfter } from '@n8n/api-types/push/execution'; import { useAssistantStore } from '@/features/ai/assistant/assistant.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import type { INodeExecutionData, ITaskData } from 'n8n-workflow'; import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow'; @@ -22,8 +20,8 @@ export async function nodeExecuteAfter( { workflowState }: { workflowState: WorkflowState }, ) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), ); const assistantStore = useAssistantStore(); @@ -60,7 +58,7 @@ export async function nodeExecuteAfter( }, }; - const activeExecutionId = stateStore.activeExecutionId; + const activeExecutionId = workflowExecutionStateStore.activeExecutionId; if (typeof activeExecutionId === 'string') { useExecutionDataStore(createExecutionDataId(activeExecutionId)).updateNodeExecutionStatus( pushDataWithPlaceholderOutputData, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.test.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.test.ts index c36dfb30ecb..f32563acb15 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteAfterData.test.ts @@ -4,15 +4,13 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import type { NodeExecuteAfterData } from '@n8n/api-types/push/execution'; import { createRunExecutionData } from 'n8n-workflow'; import { createTestWorkflowExecutionResponse } from '@/__tests__/mocks'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; describe('nodeExecuteAfterData', () => { let workflowsStore: ReturnType; - let stateStore: ReturnType; + let workflowExecutionStateStore: ReturnType; let executionDataStore: ReturnType; beforeEach(() => { @@ -21,7 +19,9 @@ describe('nodeExecuteAfterData', () => { workflowsStore = useWorkflowsStore(); workflowsStore.setWorkflowId('test-wf'); - stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('test-wf')); + workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('test-wf'), + ); executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution( @@ -49,7 +49,7 @@ describe('nodeExecuteAfterData', () => { }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); }); it('should update node execution data with incoming payload', async () => { 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 6a04c8ad545..f6ef3e00db9 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 @@ -2,10 +2,7 @@ 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 { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { createWorkflowDocumentId, @@ -17,15 +14,15 @@ import { */ export async function nodeExecuteAfterData({ data: pushData }: NodeExecuteAfterData) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), - ); const workflowDocumentStore = computed(() => useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), ); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + workflowDocumentStore.value.documentId, + ); const schemaPreviewStore = useSchemaPreviewStore(); - const activeExecutionId = stateStore.activeExecutionId; + const activeExecutionId = workflowExecutionStateStore.activeExecutionId; if (typeof activeExecutionId === 'string') { useExecutionDataStore(createExecutionDataId(activeExecutionId)).updateNodeExecutionRunData( pushData, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteBefore.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteBefore.ts index e2c55bb4e9f..9d6cc68b88a 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteBefore.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/nodeExecuteBefore.ts @@ -1,9 +1,7 @@ import type { NodeExecuteBefore } from '@n8n/api-types/push/execution'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import type { WorkflowState } from '@/app/composables/useWorkflowState'; @@ -15,13 +13,13 @@ export async function nodeExecuteBefore( { workflowState }: { workflowState: WorkflowState }, ) { const workflowsStore = useWorkflowsStore(); - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowsStore.workflowId), + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), ); workflowState.executingNode.addExecutingNode(data.nodeName); - const activeExecutionId = stateStore.activeExecutionId; + const activeExecutionId = workflowExecutionStateStore.activeExecutionId; if (typeof activeExecutionId === 'string') { useExecutionDataStore(createExecutionDataId(activeExecutionId)).addNodeExecutionStartedData( data, diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookDeleted.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookDeleted.ts index 5da3284dec5..3218d8febff 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookDeleted.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookDeleted.ts @@ -1,5 +1,7 @@ import type { TestWebhookDeleted } from '@n8n/api-types/push/webhook'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import type { WorkflowState } from '@/app/composables/useWorkflowState'; /** @@ -12,7 +14,9 @@ export async function testWebhookDeleted( const workflowsStore = useWorkflowsStore(); if (data.workflowId === workflowsStore.workflowId) { - workflowsStore.setExecutionWaitingForWebhook(false); + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setExecutionWaitingForWebhook(false); options.workflowState.setActiveExecutionId(undefined); } } diff --git a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookReceived.ts b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookReceived.ts index 19c538897c3..b426e8c99ba 100644 --- a/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookReceived.ts +++ b/packages/frontend/editor-ui/src/app/composables/usePushConnection/handlers/testWebhookReceived.ts @@ -1,5 +1,7 @@ import type { TestWebhookReceived } from '@n8n/api-types/push/webhook'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import type { WorkflowState } from '@/app/composables/useWorkflowState'; /** @@ -12,7 +14,9 @@ export async function testWebhookReceived( const workflowsStore = useWorkflowsStore(); if (data.workflowId === workflowsStore.workflowId) { - workflowsStore.setExecutionWaitingForWebhook(false); + useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ).setExecutionWaitingForWebhook(false); options.workflowState.setActiveExecutionId(data.executionId ?? null); } } 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 84b3529d5f9..9010249566d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts @@ -25,6 +25,8 @@ import type { INodeUi, IStartRunData } from '@/Interface'; import type { IExecutionResponse } from '@/features/execution/executions/executions.types'; import type { WorkflowData } from '@n8n/rest-api-client/api/workflows'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useToast } from '@/app/composables/useToast'; import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers'; @@ -53,6 +55,7 @@ type Writable = { -readonly [K in keyof T]: T[K] }; const { mockDocumentStore } = vi.hoisted(() => { const store = { + documentId: '123@latest', workflowId: '123', name: 'Test Workflow', allNodes: [], @@ -95,12 +98,12 @@ vi.mock('@/app/stores/workflowDocument.store', () => ({ })); vi.mock('@/app/stores/workflows.store', async () => { - const { createWorkflowExecutionStateId, useWorkflowExecutionStateStore } = await vi.importActual< + const { useWorkflowExecutionStateStore } = await vi.importActual< typeof import('@/app/stores/workflowExecutionState.store') >('@/app/stores/workflowExecutionState.store'); function getStateStore() { - return useWorkflowExecutionStateStore(createWorkflowExecutionStateId('123')); + return useWorkflowExecutionStateStore(createWorkflowDocumentId('123')); } const storeState: Record = { @@ -356,7 +359,9 @@ describe('useRunWorkflow({ router })', () => { expect(response).toEqual(mockResponse); expect(setActiveExecutionId).toHaveBeenNthCalledWith(1, null); expect(setActiveExecutionId).toHaveBeenNthCalledWith(2, '123'); - expect(workflowsStore.executionWaitingForWebhook).toBe(false); + expect( + useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).executionWaitingForWebhook, + ).toBe(false); }); it('should not prevent running a webhook-based workflow that has issues', async () => { @@ -393,7 +398,9 @@ describe('useRunWorkflow({ router })', () => { const response = await runWorkflowApi({} as IStartRunData); expect(response).toEqual(mockResponse); - expect(workflowsStore.executionWaitingForWebhook).toBe(true); + expect( + useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).executionWaitingForWebhook, + ).toBe(true); }); }); @@ -1259,7 +1266,9 @@ describe('useRunWorkflow({ router })', () => { const runWorkflowComposable = useRunWorkflow({ router }); mockDocumentStore.allNodes = [chatTrigger]; - vi.mocked(workflowsStore).selectedTriggerNodeName = undefined; + useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).setSelectedTriggerNodeName( + undefined, + ); mockDocumentStore.serialize.mockReturnValue({ id: 'workflowId', nodes: [], @@ -1289,7 +1298,9 @@ describe('useRunWorkflow({ router })', () => { const runWorkflowComposable = useRunWorkflow({ router }); mockDocumentStore.allNodes = [chatTrigger, manualTrigger]; - vi.mocked(workflowsStore).selectedTriggerNodeName = undefined; + useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).setSelectedTriggerNodeName( + undefined, + ); mockDocumentStore.serialize.mockReturnValue({ id: 'workflowId', nodes: [], @@ -1390,7 +1401,9 @@ describe('useRunWorkflow({ router })', () => { 'test-wf-id', ); workflowState.setActiveExecutionId('test-exec-id'); - workflowsStore.setExecutionWaitingForWebhook(false); + useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).setExecutionWaitingForWebhook( + false, + ); getExecutionSpy.mockResolvedValue(executionData); diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts index 3ccd781163a..7109b94fa6f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts @@ -22,6 +22,7 @@ import { BINARY_MODE_COMBINED, } from 'n8n-workflow'; import { retry } from '@n8n/utils/retry'; +import { computed } from 'vue'; import { useToast } from '@/app/composables/useToast'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; @@ -37,6 +38,7 @@ import { import { useRootStore } from '@n8n/stores/useRootStore'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { displayForm } from '@/features/execution/executions/executions.utils'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; @@ -76,12 +78,14 @@ export function useRunWorkflow(useRunWorkflowOpts: { const rootStore = useRootStore(); const pushConnectionStore = usePushConnectionStore(); const workflowsStore = useWorkflowsStore(); + const workflowDocumentStore = injectWorkflowDocumentStore(); + const workflowExecutionState = computed(() => + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), + ); // `inject()` only resolves inside a setup context; callers from async event // handlers must pass `workflowState` in. const workflowState = useRunWorkflowOpts.workflowState ?? injectWorkflowState(); - const workflowDocumentStore = injectWorkflowDocumentStore(); - const nodeHelpers = useNodeHelpers(); const workflowSaving = useWorkflowSaving({ router: useRunWorkflowOpts.router, @@ -124,14 +128,15 @@ export function useRunWorkflow(useRunWorkflowOpts: { throw error; } - const workflowExecutionIdIsNew = workflowsStore.previousExecutionId !== response.executionId; - const workflowExecutionIdIsPending = workflowsStore.activeExecutionId === null; + const workflowExecutionIdIsNew = + workflowExecutionState.value.previousExecutionId !== response.executionId; + const workflowExecutionIdIsPending = workflowExecutionState.value.activeExecutionId === null; if (response.executionId && workflowExecutionIdIsNew && workflowExecutionIdIsPending) { workflowState.setActiveExecutionId(response.executionId); } if (response.waitingForWebhook === true) { - workflowsStore.setExecutionWaitingForWebhook(true); + workflowExecutionState.value.setExecutionWaitingForWebhook(true); } return response; @@ -145,7 +150,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { source?: string; sessionId?: string; }): Promise { - if (workflowsStore.activeExecutionId) { + if (workflowExecutionState.value.activeExecutionId) { return; } @@ -248,7 +253,9 @@ export function useRunWorkflow(useRunWorkflowOpts: { // If the chat node has no input data or pin data, open the chat modal // and halt the execution if (!chatHasInputData && !chatHasPinData) { - workflowsStore.setChatPartialExecutionDestinationNode(options.destinationNode.nodeName); + workflowExecutionState.value.setChatPartialExecutionDestinationNode( + options.destinationNode.nodeName, + ); startChat(); return; } @@ -513,7 +520,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { } async function stopCurrentExecution() { - const executionId = workflowsStore.activeExecutionId; + const executionId = workflowExecutionState.value.activeExecutionId; let stopData: IExecutionsStopData | undefined; if (!executionId) { @@ -603,7 +610,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { telemetry.track('User clicked execute workflow button', telemetryPayload); void externalHooks.run('nodeView.onRunWorkflow', telemetryPayload); - let resolvedTriggerNode = triggerNode ?? workflowsStore.selectedTriggerNodeName; + let resolvedTriggerNode = triggerNode ?? workflowExecutionState.value.selectedTriggerNodeName; // When no trigger is explicitly selected (e.g. chat trigger is the only trigger // and the Run button doesn't offer it for selection), resolve it from the workflow. diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts index 72c9454ac30..8dc8541ea60 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts @@ -8,6 +8,7 @@ import { useExternalHooks } from '@/app/composables/useExternalHooks'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; import { useParentFolder } from '@/features/core/folders/composables/useParentFolder'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; @@ -196,11 +197,14 @@ export function useWorkflowInitialization(workflowState: WorkflowState) { documentTitle.setDocumentTitle(currentWorkflowDocumentStore.value?.name ?? '', 'DEBUG'); - if (!workflowsStore.isInDebugMode) { + const executionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + if (!executionStateStore.isInDebugMode) { const executionId = route.params.executionId; if (typeof executionId === 'string') { await applyExecutionData(executionId); - workflowsStore.setIsInDebugMode(true); + executionStateStore.setIsInDebugMode(true); } } } diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.test.ts index 8452301db46..628a75bdc92 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.test.ts @@ -3,17 +3,15 @@ import { useWorkflowState, type WorkflowState } from './useWorkflowState'; import { createPinia, setActivePinia } from 'pinia'; import { createTestTaskData, createTestWorkflowExecutionResponse } from '@/__tests__/mocks'; import { createRunExecutionData } from 'n8n-workflow'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants/placeholders'; describe('useWorkflowState', () => { let workflowsStore: ReturnType; let workflowState: WorkflowState; - let stateStore: ReturnType; + let workflowExecutionStateStore: ReturnType; let executionDataStore: ReturnType; beforeEach(() => { @@ -23,13 +21,15 @@ describe('useWorkflowState', () => { workflowsStore.setWorkflowId('test-wf'); workflowState = useWorkflowState(); - stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('test-wf')); + workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('test-wf'), + ); }); describe('markExecutionAsStopped', () => { beforeEach(() => { // Set up active execution in the facade stores - stateStore.setActiveExecutionId('test-exec-id'); + workflowExecutionStateStore.setActiveExecutionId('test-exec-id'); executionDataStore = useExecutionDataStore(createExecutionDataId('test-exec-id')); executionDataStore.setExecution( @@ -91,15 +91,15 @@ describe('useWorkflowState', () => { describe('when activeExecutionId is null (pending scaffold)', () => { beforeEach(() => { // Reset to pending state instead of the string-id default from outer beforeEach. - stateStore.setActiveExecutionId(undefined); - stateStore.setPendingExecution( + workflowExecutionStateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setPendingExecution( createTestWorkflowExecutionResponse({ id: IN_PROGRESS_EXECUTION_ID, status: 'running', }), ); // Re-set since promotePendingExecution would have moved it; emulate raw scaffold state. - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( createTestWorkflowExecutionResponse({ @@ -143,9 +143,13 @@ describe('useWorkflowState', () => { mode: 'manual', }); - expect(stateStore.pendingExecution?.status).toBe('canceled'); - expect(stateStore.pendingExecution?.startedAt).toEqual(new Date('2023-01-01T10:00:00Z')); - expect(stateStore.pendingExecution?.stoppedAt).toEqual(new Date('2023-01-01T10:05:00Z')); + expect(workflowExecutionStateStore.pendingExecution?.status).toBe('canceled'); + expect(workflowExecutionStateStore.pendingExecution?.startedAt).toEqual( + new Date('2023-01-01T10:00:00Z'), + ); + expect(workflowExecutionStateStore.pendingExecution?.stoppedAt).toEqual( + new Date('2023-01-01T10:05:00Z'), + ); }); }); @@ -153,10 +157,10 @@ describe('useWorkflowState', () => { beforeEach(() => { // Simulate post-stop-race: active was just cleared, but displayed still points // at the freshly-fetched finished execution. - stateStore.setActiveExecutionId('display-exec'); - stateStore.setActiveExecutionId(undefined); - expect(stateStore.activeExecutionId).toBeUndefined(); - expect(stateStore.displayedExecutionId).toBe('display-exec'); + workflowExecutionStateStore.setActiveExecutionId('display-exec'); + workflowExecutionStateStore.setActiveExecutionId(undefined); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('display-exec'); useExecutionDataStore(createExecutionDataId('display-exec')).setExecution( createTestWorkflowExecutionResponse({ @@ -195,15 +199,15 @@ describe('useWorkflowState', () => { describe('resetState', () => { it('disposes every executionData store this workflow ever bound, including rolled-out ids', () => { // Three sequential runs — exec-1 rolls out of previousExecutionId after run 3. - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( createTestWorkflowExecutionResponse({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-2'); + workflowExecutionStateStore.setActiveExecutionId('exec-2'); useExecutionDataStore(createExecutionDataId('exec-2')).setExecution( createTestWorkflowExecutionResponse({ id: 'exec-2' }), ); - stateStore.setActiveExecutionId('exec-3'); + workflowExecutionStateStore.setActiveExecutionId('exec-3'); useExecutionDataStore(createExecutionDataId('exec-3')).setExecution( createTestWorkflowExecutionResponse({ id: 'exec-3' }), ); @@ -216,18 +220,18 @@ describe('useWorkflowState', () => { }); it('clears displayedExecutionId so workflowExecutionData reads as null', () => { - stateStore.setActiveExecutionId('exec-A'); + workflowExecutionStateStore.setActiveExecutionId('exec-A'); useExecutionDataStore(createExecutionDataId('exec-A')).setExecution( createTestWorkflowExecutionResponse({ id: 'exec-A', finished: true, status: 'success' }), ); // Simulate post-finish: active cleared, displayed preserved (the deliberate UX behavior). - stateStore.setActiveExecutionId(undefined); - expect(stateStore.displayedExecutionId).toBe('exec-A'); + workflowExecutionStateStore.setActiveExecutionId(undefined); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-A'); expect(workflowsStore.workflowExecutionData?.id).toBe('exec-A'); workflowState.resetState(); - const fresh = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('test-wf')); + const fresh = useWorkflowExecutionStateStore(createWorkflowDocumentId('test-wf')); expect(fresh.displayedExecutionId).toBeUndefined(); expect(fresh.activeExecutionId).toBeUndefined(); expect(fresh.pendingExecution).toBeNull(); @@ -235,7 +239,7 @@ describe('useWorkflowState', () => { }); it('disposes the IN_PROGRESS placeholder store along with the pending scaffold', () => { - stateStore.setPendingExecution( + workflowExecutionStateStore.setPendingExecution( createTestWorkflowExecutionResponse({ id: IN_PROGRESS_EXECUTION_ID, status: 'running', @@ -260,11 +264,11 @@ describe('useWorkflowState', () => { it('reopening the same workflow id after resetWorkspace order surfaces no stale state', () => { // Stage execution on test-wf - stateStore.setActiveExecutionId('exec-A'); + workflowExecutionStateStore.setActiveExecutionId('exec-A'); useExecutionDataStore(createExecutionDataId('exec-A')).setExecution( createTestWorkflowExecutionResponse({ id: 'exec-A', finished: true, status: 'success' }), ); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId(undefined); expect(workflowsStore.workflowExecutionData?.id).toBe('exec-A'); // Mirror resetWorkspace ordering: resetState first (while workflowId is still set), @@ -277,7 +281,7 @@ describe('useWorkflowState', () => { workflowsStore.setWorkflowId('test-wf'); // Fresh state — no leakage. - const fresh = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('test-wf')); + const fresh = useWorkflowExecutionStateStore(createWorkflowDocumentId('test-wf')); expect(fresh.activeExecutionId).toBeUndefined(); expect(fresh.displayedExecutionId).toBeUndefined(); expect(fresh.pendingExecution).toBeNull(); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts index 09e138b1727..0a8648ca02d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts @@ -8,7 +8,6 @@ import { DEFAULT_SETTINGS } from '@/app/stores/workflowDocument/useWorkflowDocum import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowStateStore } from '@/app/stores/workflowState.store'; import { - createWorkflowExecutionStateId, disposeWorkflowExecutionStateStore, useWorkflowExecutionStateStore, } from '@/app/stores/workflowExecutionState.store'; @@ -37,35 +36,35 @@ export function useWorkflowState() { //// function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) { - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(ws.workflowId), + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(ws.workflowId), ); if (workflowResultData === null) { - stateStore.setPendingExecution(null); - stateStore.clearDisplayedExecution(); + workflowExecutionStateStore.setPendingExecution(null); + workflowExecutionStateStore.clearDisplayedExecution(); } else if (workflowResultData.id === IN_PROGRESS_EXECUTION_ID) { - stateStore.setPendingExecution(workflowResultData); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setPendingExecution(workflowResultData); + workflowExecutionStateStore.setActiveExecutionId(null); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( workflowResultData, ); } else { - stateStore.trackExecutionId(workflowResultData.id); + workflowExecutionStateStore.trackExecutionId(workflowResultData.id); useExecutionDataStore(createExecutionDataId(workflowResultData.id)).setExecution( workflowResultData, ); - if (typeof stateStore.activeExecutionId !== 'string') { - stateStore.setPendingExecution(null); - stateStore.setActiveExecutionId(undefined); - stateStore.setDisplayedExecutionId(workflowResultData.id); + if (typeof workflowExecutionStateStore.activeExecutionId !== 'string') { + workflowExecutionStateStore.setPendingExecution(null); + workflowExecutionStateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setDisplayedExecutionId(workflowResultData.id); } } } function setActiveExecutionId(id: string | null | undefined) { - useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(ws.workflowId), - ).setActiveExecutionId(id); + useWorkflowExecutionStateStore(createWorkflowDocumentId(ws.workflowId)).setActiveExecutionId( + id, + ); } async function getNewWorkflowData( @@ -103,16 +102,16 @@ export function useWorkflowState() { const documentTitle = useDocumentTitle(); function markExecutionAsStopped(stopData?: IExecutionsStopData) { - const stateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(ws.workflowId), - ); - const activeExecutionId = stateStore.activeExecutionId; - - stateStore.setActiveExecutionId(undefined); - workflowStateStore.executingNode.clearNodeExecutionQueue(); - stateStore.setExecutionWaitingForWebhook(false); - const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(ws.workflowId)); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + workflowDocumentStore.documentId, + ); + const activeExecutionId = workflowExecutionStateStore.activeExecutionId; + + workflowExecutionStateStore.setActiveExecutionId(undefined); + workflowStateStore.executingNode.clearNodeExecutionQueue(); + workflowExecutionStateStore.setExecutionWaitingForWebhook(false); + documentTitle.setDocumentTitle(workflowDocumentStore.name, 'IDLE'); if (typeof activeExecutionId === 'string') { @@ -128,12 +127,12 @@ export function useWorkflowState() { executionDataStore.clearExecutionStartedData(); executionDataStore.markAsStopped(stopData); if (stopData) { - stateStore.applyStopDataToPendingExecution(stopData); + workflowExecutionStateStore.applyStopDataToPendingExecution(stopData); } } else { // activeExecutionId === undefined: fall back to displayedExecutionId for the // stop-race-with-finished case where active was just cleared. - const displayedExecutionId = stateStore.displayedExecutionId; + const displayedExecutionId = workflowExecutionStateStore.displayedExecutionId; if (typeof displayedExecutionId === 'string') { const executionDataStore = useExecutionDataStore( createExecutionDataId(displayedExecutionId), @@ -153,13 +152,15 @@ export function useWorkflowState() { useBuilderStore().resetManualExecutionStats(); return; } - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId(wid)); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(wid), + ); // Disposes every tracked executionData store + IN_PROGRESS placeholder, then clears all // session-level fields. - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); // Then dispose the per-workflow state store so pinia state doesn't accumulate one entry // per workflow ever opened in this session. - disposeWorkflowExecutionStateStore(stateStore); + disposeWorkflowExecutionStateStore(workflowExecutionStateStore); workflowStateStore.executingNode.executingNode.length = 0; useBuilderStore().resetManualExecutionStats(); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.test.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.test.ts index c29c9a1be97..bd10c8d93f1 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.test.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.test.ts @@ -7,6 +7,7 @@ import { useWorkflowDocumentStore, createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useWorkflowDocumentRenderData } from './useWorkflowDocumentRenderData'; const nodeInputsByNodeId = shallowReactive(new Map>()); @@ -31,13 +32,13 @@ vi.mock('@/app/stores/workflowExecutionState.store', () => ({ useWorkflowExecutionStateStore: vi.fn(() => ({ activeExecutionIssuesByNodeName: executionIssuesByNodeName, })), - createWorkflowExecutionStateId: (id: string) => id, })); describe('useWorkflowDocumentRenderData', () => { beforeEach(() => { setActivePinia(createPinia()); vi.mocked(useWorkflowDocumentStore).mockClear(); + vi.mocked(useWorkflowExecutionStateStore).mockClear(); }); it('passes through nodeInputsByNodeId and nodeOutputsByNodeId by reference', () => { @@ -58,4 +59,12 @@ describe('useWorkflowDocumentRenderData', () => { expect(renderData.executionIssuesByNodeName).toBe(executionIssuesByNodeName); }); + + it('uses the exact workflow document id when resolving execution state', () => { + const documentId = createWorkflowDocumentId('wf-1', 'ver-123'); + + useWorkflowDocumentRenderData(documentId); + + expect(useWorkflowExecutionStateStore).toHaveBeenCalledWith(documentId); + }); }); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.ts index fbb4df85b1b..c9394679dab 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentRenderData.ts @@ -2,10 +2,7 @@ import { useWorkflowDocumentStore, type WorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; -import { - useWorkflowExecutionStateStore, - createWorkflowExecutionStateId, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; /** * Canvas render data accessor for a workflow document. @@ -25,9 +22,7 @@ import { */ export function useWorkflowDocumentRenderData(workflowDocumentId: WorkflowDocumentId) { const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId); - const executionStateStore = useWorkflowExecutionStateStore( - createWorkflowExecutionStateId(workflowDocumentStore.workflowId), - ); + const executionStateStore = useWorkflowExecutionStateStore(workflowDocumentId); return { nodeInputsByNodeId: workflowDocumentStore.nodeInputsByNodeId, diff --git a/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.test.ts b/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.test.ts index 4b8707ee67b..14a30a0f8e9 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.test.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.test.ts @@ -10,10 +10,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { setActivePinia, createPinia, getActivePinia } from 'pinia'; import { useWorkflowExecutionStateStore, - createWorkflowExecutionStateId, getWorkflowExecutionStateStoreId, disposeWorkflowExecutionStateStore, } from '@/app/stores/workflowExecutionState.store'; +import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store'; import { useExecutionDataStore, createExecutionDataId } from '@/app/stores/executionData.store'; import { createTestWorkflowExecutionResponse } from '@/__tests__/mocks'; import type { IExecutionResponse } from '@/features/execution/executions/executions.types'; @@ -48,14 +48,16 @@ describe('workflowExecutionState.store', () => { describe('store identity', () => { it('uses a workflowId-scoped store id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-42')); - expect(stateStore.$id).toBe(getWorkflowExecutionStateStoreId('wf-42')); - expect(stateStore.workflowId).toBe('wf-42'); + const id = createWorkflowDocumentId('wf-42'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore(id); + expect(workflowExecutionStateStore.$id).toBe(getWorkflowExecutionStateStoreId(id)); + expect(workflowExecutionStateStore.documentId).toBe(id); + expect(workflowExecutionStateStore.workflowId).toBe('wf-42'); }); it('different workflowIds produce isolated state stores', () => { - const a = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-a')); - const b = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-b')); + const a = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-a')); + const b = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-b')); a.setExecutionWaitingForWebhook(true); b.setIsInDebugMode(true); @@ -69,86 +71,108 @@ describe('workflowExecutionState.store', () => { describe('activeExecutionId tri-state', () => { it('starts undefined', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionId).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); }); it('null indicates execution started but id pending', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId(null); - expect(stateStore.activeExecutionId).toBeNull(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId(null); + expect(workflowExecutionStateStore.activeExecutionId).toBeNull(); }); it('string indicates known execution id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecutionId).toBe('exec-1'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1'); }); it('rolls activeExecutionId into previousExecutionId on transition to a new id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId('exec-2'); - expect(stateStore.previousExecutionId).toBe('exec-1'); - expect(stateStore.activeExecutionId).toBe('exec-2'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-2'); + expect(workflowExecutionStateStore.previousExecutionId).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-2'); }); it('does not update previousExecutionId when clearing to undefined', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); - expect(stateStore.previousExecutionId).toBeUndefined(); - expect(stateStore.activeExecutionId).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); + expect(workflowExecutionStateStore.previousExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); }); it('setting a string id also sets displayedExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - expect(stateStore.displayedExecutionId).toBe('exec-1'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-1'); }); it('clearing activeExecutionId preserves displayedExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); - expect(stateStore.displayedExecutionId).toBe('exec-1'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-1'); }); }); describe('activeExecution routing', () => { it('returns pendingExecution when activeExecutionId === null', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const scaffold = makeExecution({ id: '__IN_PROGRESS__' }); - stateStore.setPendingExecution(scaffold); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setPendingExecution(scaffold); + workflowExecutionStateStore.setActiveExecutionId(null); - expect(stateStore.activeExecution?.id).toBe('__IN_PROGRESS__'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('__IN_PROGRESS__'); }); it('returns the executionData store entry when activeExecutionId is a string', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution(makeExecution({ id: 'exec-1' })); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecution?.id).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('exec-1'); }); it('falls back to displayed execution after active is cleared', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution(makeExecution({ id: 'exec-1' })); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); - expect(stateStore.activeExecution?.id).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('exec-1'); }); it('returns null when nothing is set', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecution).toBeNull(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecution).toBeNull(); }); }); @@ -159,7 +183,9 @@ describe('workflowExecutionState.store', () => { // timestamp on the targeted data store and nothing else. describe('resolver fallback', () => { it('routes string activeExecutionId → that id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-A')).setExecution( makeExecution({ id: 'exec-A' }), ); @@ -167,13 +193,15 @@ describe('workflowExecutionState.store', () => { createExecutionDataId('exec-A'), ).executionResultDataLastUpdate; - stateStore.setActiveExecutionId('exec-A'); + workflowExecutionStateStore.setActiveExecutionId('exec-A'); - expect(stateStore.activeExecutionResultDataLastUpdate).toBe(expected); + expect(workflowExecutionStateStore.activeExecutionResultDataLastUpdate).toBe(expected); }); it('routes null activeExecutionId → IN_PROGRESS sentinel', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( makeExecution({ id: IN_PROGRESS_EXECUTION_ID }), ); @@ -181,13 +209,15 @@ describe('workflowExecutionState.store', () => { createExecutionDataId(IN_PROGRESS_EXECUTION_ID), ).executionResultDataLastUpdate; - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); - expect(stateStore.activeExecutionResultDataLastUpdate).toBe(expected); + expect(workflowExecutionStateStore.activeExecutionResultDataLastUpdate).toBe(expected); }); it('routes undefined activeExecutionId with string displayedExecutionId → displayed id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-D')).setExecution( makeExecution({ id: 'exec-D' }), ); @@ -195,108 +225,128 @@ describe('workflowExecutionState.store', () => { createExecutionDataId('exec-D'), ).executionResultDataLastUpdate; - stateStore.setActiveExecutionId('exec-D'); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('exec-D'); + workflowExecutionStateStore.setActiveExecutionId(undefined); - expect(stateStore.activeExecutionId).toBeUndefined(); - expect(stateStore.displayedExecutionId).toBe('exec-D'); - expect(stateStore.activeExecutionResultDataLastUpdate).toBe(expected); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-D'); + expect(workflowExecutionStateStore.activeExecutionResultDataLastUpdate).toBe(expected); }); it('returns undefined when nothing is set', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionResultDataLastUpdate).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionResultDataLastUpdate).toBeUndefined(); }); }); describe('activeExecutionRunData', () => { it('proxies through the executionData store for the active id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', data: { resultData: { runData: { Trigger: [] } } } as never, }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecutionRunData).toEqual({ Trigger: [] }); + expect(workflowExecutionStateStore.activeExecutionRunData).toEqual({ Trigger: [] }); }); it('falls back to IN_PROGRESS for null activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( makeExecution({ id: IN_PROGRESS_EXECUTION_ID, data: { resultData: { runData: { Pending: [] } } } as never, }), ); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); - expect(stateStore.activeExecutionRunData).toEqual({ Pending: [] }); + expect(workflowExecutionStateStore.activeExecutionRunData).toEqual({ Pending: [] }); }); it('falls back to displayed id after active is cleared', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', data: { resultData: { runData: { Displayed: [] } } } as never, }), ); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); - expect(stateStore.activeExecutionRunData).toEqual({ Displayed: [] }); + expect(workflowExecutionStateStore.activeExecutionRunData).toEqual({ Displayed: [] }); }); it('returns null when no execution is tracked', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionRunData).toBeNull(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionRunData).toBeNull(); }); }); describe('activeExecutionExecutedNode', () => { it('proxies the executedNode from the active executionData store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', executedNode: 'Code' }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecutionExecutedNode).toBe('Code'); + expect(workflowExecutionStateStore.activeExecutionExecutedNode).toBe('Code'); }); it('falls back to IN_PROGRESS for null activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( makeExecution({ id: IN_PROGRESS_EXECUTION_ID, executedNode: 'Pending' }), ); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); - expect(stateStore.activeExecutionExecutedNode).toBe('Pending'); + expect(workflowExecutionStateStore.activeExecutionExecutedNode).toBe('Pending'); }); it('falls back to displayed id after active is cleared', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', executedNode: 'Trigger' }), ); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); - expect(stateStore.activeExecutionExecutedNode).toBe('Trigger'); + expect(workflowExecutionStateStore.activeExecutionExecutedNode).toBe('Trigger'); }); it('returns undefined when nothing is set', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionExecutedNode).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionExecutedNode).toBeUndefined(); }); }); describe('activeExecutionStartedData', () => { it('proxies executionStartedData for string activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution(makeExecution({ id: 'exec-1' })); executionDataStore.addNodeExecutionStartedData({ @@ -304,14 +354,16 @@ describe('workflowExecutionState.store', () => { nodeName: 'Code', data: { startTime: 1 } as never, }); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecutionStartedData?.[0]).toBe('exec-1'); - expect(stateStore.activeExecutionStartedData?.[1].Code).toHaveLength(1); + expect(workflowExecutionStateStore.activeExecutionStartedData?.[0]).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecutionStartedData?.[1].Code).toHaveLength(1); }); it('falls back to IN_PROGRESS for null activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore( createExecutionDataId(IN_PROGRESS_EXECUTION_ID), ); @@ -321,94 +373,110 @@ describe('workflowExecutionState.store', () => { nodeName: 'Pending', data: { startTime: 1 } as never, }); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setActiveExecutionId(null); - expect(stateStore.activeExecutionStartedData?.[0]).toBe(IN_PROGRESS_EXECUTION_ID); - expect(stateStore.activeExecutionStartedData?.[1].Pending).toHaveLength(1); + expect(workflowExecutionStateStore.activeExecutionStartedData?.[0]).toBe( + IN_PROGRESS_EXECUTION_ID, + ); + expect(workflowExecutionStateStore.activeExecutionStartedData?.[1].Pending).toHaveLength(1); }); it('returns undefined when nothing is set', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionStartedData).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionStartedData).toBeUndefined(); }); }); describe('activeExecutionPairedItemMappings', () => { it('proxies pairedItemMappings for string activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); const expected = useExecutionDataStore( createExecutionDataId('exec-1'), ).executionPairedItemMappings; - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.activeExecutionPairedItemMappings).toBe(expected); + expect(workflowExecutionStateStore.activeExecutionPairedItemMappings).toBe(expected); }); it('returns empty object when no execution is tracked', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.activeExecutionPairedItemMappings).toEqual({}); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.activeExecutionPairedItemMappings).toEqual({}); }); }); }); describe('setActiveExecution', () => { it('null clears pending and displayed execution ids', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setPendingExecution(makeExecution()); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setPendingExecution(makeExecution()); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecution(null); + workflowExecutionStateStore.setActiveExecution(null); - expect(stateStore.pendingExecution).toBeNull(); - expect(stateStore.displayedExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.displayedExecutionId).toBeUndefined(); }); it('IN_PROGRESS payload stages pending + activeExecutionId=null + writes IN_PROGRESS data store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const payload = makeExecution({ id: IN_PROGRESS_EXECUTION_ID, executedNode: 'Code' }); - stateStore.setActiveExecution(payload); + workflowExecutionStateStore.setActiveExecution(payload); - expect(stateStore.activeExecutionId).toBeNull(); - expect(stateStore.pendingExecution?.id).toBe(IN_PROGRESS_EXECUTION_ID); + expect(workflowExecutionStateStore.activeExecutionId).toBeNull(); + expect(workflowExecutionStateStore.pendingExecution?.id).toBe(IN_PROGRESS_EXECUTION_ID); expect( useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).execution?.id, ).toBe(IN_PROGRESS_EXECUTION_ID); }); it('real id without prior active sets data store, clears pending, promotes displayed id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setPendingExecution(makeExecution()); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setPendingExecution(makeExecution()); const exec = makeExecution({ id: 'exec-real' }); - stateStore.setActiveExecution(exec); + workflowExecutionStateStore.setActiveExecution(exec); expect(useExecutionDataStore(createExecutionDataId('exec-real')).execution?.id).toBe( 'exec-real', ); - expect(stateStore.pendingExecution).toBeNull(); - expect(stateStore.activeExecutionId).toBeUndefined(); - expect(stateStore.displayedExecutionId).toBe('exec-real'); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-real'); }); it('real id while activeExecutionId is already a string updates the data store only', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); const updated = makeExecution({ id: 'exec-1', executedNode: 'Code' }); - stateStore.setActiveExecution(updated); + workflowExecutionStateStore.setActiveExecution(updated); - expect(stateStore.activeExecutionId).toBe('exec-1'); - expect(stateStore.displayedExecutionId).toBe('exec-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1'); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-1'); expect(useExecutionDataStore(createExecutionDataId('exec-1')).execution?.executedNode).toBe( 'Code', ); @@ -417,13 +485,15 @@ describe('workflowExecutionState.store', () => { describe('setActiveExecutionRunData / clearActiveExecutionStartedData / addActiveNodeExecutionStartedData', () => { it('setActiveExecutionRunData routes through the resolved id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionRunData({ + workflowExecutionStateStore.setActiveExecutionRunData({ resultData: { runData: { Code: [] } }, } as never); @@ -433,13 +503,15 @@ describe('workflowExecutionState.store', () => { }); it('addActiveNodeExecutionStartedData routes through the resolved id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - stateStore.addActiveNodeExecutionStartedData({ + workflowExecutionStateStore.addActiveNodeExecutionStartedData({ executionId: 'exec-1', nodeName: 'Code', data: { startTime: 1 } as never, @@ -451,7 +523,9 @@ describe('workflowExecutionState.store', () => { }); it('clearActiveExecutionStartedData routes through the resolved id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution(makeExecution({ id: 'exec-1' })); executionDataStore.addNodeExecutionStartedData({ @@ -459,22 +533,26 @@ describe('workflowExecutionState.store', () => { nodeName: 'Code', data: { startTime: 1 } as never, }); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - stateStore.clearActiveExecutionStartedData(); + workflowExecutionStateStore.clearActiveExecutionStartedData(); expect(executionDataStore.executionStartedData).toBeUndefined(); }); it('writes go nowhere when no execution is tracked', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); expect(() => - stateStore.setActiveExecutionRunData({ resultData: { runData: {} } } as never), + workflowExecutionStateStore.setActiveExecutionRunData({ + resultData: { runData: {} }, + } as never), ).not.toThrow(); - expect(() => stateStore.clearActiveExecutionStartedData()).not.toThrow(); + expect(() => workflowExecutionStateStore.clearActiveExecutionStartedData()).not.toThrow(); expect(() => - stateStore.addActiveNodeExecutionStartedData({ + workflowExecutionStateStore.addActiveNodeExecutionStartedData({ executionId: 'x', nodeName: 'N', data: { startTime: 1 } as never, @@ -485,20 +563,22 @@ describe('workflowExecutionState.store', () => { describe('promotePendingExecution', () => { it('migrates the pending scaffold into a fresh executionData store and sets activeExecutionId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const scaffold = makeExecution({ id: '__IN_PROGRESS__', data: { resultData: { runData: { Trigger: [{ executionStatus: 'success' } as never] } }, } as never, }); - stateStore.setPendingExecution(scaffold); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setPendingExecution(scaffold); + workflowExecutionStateStore.setActiveExecutionId(null); - stateStore.promotePendingExecution('exec-real'); + workflowExecutionStateStore.promotePendingExecution('exec-real'); - expect(stateStore.activeExecutionId).toBe('exec-real'); - expect(stateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-real'); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-real')); expect(executionDataStore.execution?.id).toBe('exec-real'); @@ -506,16 +586,20 @@ describe('workflowExecutionState.store', () => { }); it('still sets activeExecutionId when no pending scaffold exists', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); - stateStore.promotePendingExecution('exec-real'); + workflowExecutionStateStore.promotePendingExecution('exec-real'); - expect(stateStore.activeExecutionId).toBe('exec-real'); - expect(stateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-real'); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); }); it('setActiveExecutionId(string) migrates a staged pending scaffold into the id-keyed store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const scaffold = makeExecution({ id: '__IN_PROGRESS__', executedNode: 'Code', @@ -523,13 +607,13 @@ describe('workflowExecutionState.store', () => { resultData: { runData: {} }, } as never, }); - stateStore.setPendingExecution(scaffold); - stateStore.setActiveExecutionId(null); + workflowExecutionStateStore.setPendingExecution(scaffold); + workflowExecutionStateStore.setActiveExecutionId(null); - stateStore.setActiveExecutionId('exec-real'); + workflowExecutionStateStore.setActiveExecutionId('exec-real'); - expect(stateStore.activeExecutionId).toBe('exec-real'); - expect(stateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-real'); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-real')); expect(executionDataStore.execution?.id).toBe('exec-real'); @@ -537,12 +621,14 @@ describe('workflowExecutionState.store', () => { }); it('setActiveExecutionId(string) without a pending scaffold does not promote', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); - stateStore.setActiveExecutionId('exec-real'); + workflowExecutionStateStore.setActiveExecutionId('exec-real'); - expect(stateStore.activeExecutionId).toBe('exec-real'); - expect(stateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-real'); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-real')); expect(executionDataStore.execution).toBeNull(); }); @@ -550,231 +636,282 @@ describe('workflowExecutionState.store', () => { describe('isWorkflowRunning', () => { it('is true when activeExecutionId is null (pending)', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId(null); - expect(stateStore.isWorkflowRunning).toBe(true); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId(null); + expect(workflowExecutionStateStore.isWorkflowRunning).toBe(true); }); it('is true when active execution is running and not finished', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', status: 'running', finished: false }), ); - stateStore.setActiveExecutionId('exec-1'); - expect(stateStore.isWorkflowRunning).toBe(true); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + expect(workflowExecutionStateStore.isWorkflowRunning).toBe(true); }); it('is false when active execution is finished', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', status: 'success', finished: true }), ); - stateStore.setActiveExecutionId('exec-1'); - expect(stateStore.isWorkflowRunning).toBe(false); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + expect(workflowExecutionStateStore.isWorkflowRunning).toBe(false); }); it('is false when no active execution is tracked', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.isWorkflowRunning).toBe(false); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect(workflowExecutionStateStore.isWorkflowRunning).toBe(false); }); }); describe('currentWorkflowExecutions', () => { it('addToCurrentExecutions filters by workflowId', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.addToCurrentExecutions([ + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.addToCurrentExecutions([ makeExecutionSummary({ id: '1', workflowId: 'wf-1' }), makeExecutionSummary({ id: '2', workflowId: 'other-wf' }), ]); - expect(stateStore.currentWorkflowExecutions).toHaveLength(1); - expect(stateStore.currentWorkflowExecutions[0].id).toBe('1'); + expect(workflowExecutionStateStore.currentWorkflowExecutions).toHaveLength(1); + expect(workflowExecutionStateStore.currentWorkflowExecutions[0].id).toBe('1'); }); it('addToCurrentExecutions deduplicates by id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.addToCurrentExecutions([makeExecutionSummary({ id: '1', workflowId: 'wf-1' })]); - stateStore.addToCurrentExecutions([ + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.addToCurrentExecutions([ + makeExecutionSummary({ id: '1', workflowId: 'wf-1' }), + ]); + workflowExecutionStateStore.addToCurrentExecutions([ makeExecutionSummary({ id: '1', workflowId: 'wf-1' }), makeExecutionSummary({ id: '2', workflowId: 'wf-1' }), ]); - expect(stateStore.currentWorkflowExecutions.map((e) => e.id)).toEqual(['1', '2']); + expect(workflowExecutionStateStore.currentWorkflowExecutions.map((e) => e.id)).toEqual([ + '1', + '2', + ]); }); it('deleteExecution accepts both summary and id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const a = makeExecutionSummary({ id: '1', workflowId: 'wf-1' }); const b = makeExecutionSummary({ id: '2', workflowId: 'wf-1' }); - stateStore.setCurrentWorkflowExecutions([a, b]); + workflowExecutionStateStore.setCurrentWorkflowExecutions([a, b]); - stateStore.deleteExecution(a); - expect(stateStore.currentWorkflowExecutions.map((e) => e.id)).toEqual(['2']); + workflowExecutionStateStore.deleteExecution(a); + expect(workflowExecutionStateStore.currentWorkflowExecutions.map((e) => e.id)).toEqual(['2']); - stateStore.deleteExecution('2'); - expect(stateStore.currentWorkflowExecutions).toEqual([]); + workflowExecutionStateStore.deleteExecution('2'); + expect(workflowExecutionStateStore.currentWorkflowExecutions).toEqual([]); }); it('clearCurrentWorkflowExecutions empties the list', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setCurrentWorkflowExecutions([makeExecutionSummary({ id: '1' })]); - stateStore.clearCurrentWorkflowExecutions(); - expect(stateStore.currentWorkflowExecutions).toEqual([]); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setCurrentWorkflowExecutions([makeExecutionSummary({ id: '1' })]); + workflowExecutionStateStore.clearCurrentWorkflowExecutions(); + expect(workflowExecutionStateStore.currentWorkflowExecutions).toEqual([]); }); it('getAllLoadedFinishedExecutions filters by finished or stoppedAt', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setCurrentWorkflowExecutions([ + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setCurrentWorkflowExecutions([ makeExecutionSummary({ id: '1', finished: true }), makeExecutionSummary({ id: '2', finished: false }), makeExecutionSummary({ id: '3', finished: false, stoppedAt: new Date() }), ]); - expect(stateStore.getAllLoadedFinishedExecutions.map((e) => e.id).sort()).toEqual(['1', '3']); + expect( + workflowExecutionStateStore.getAllLoadedFinishedExecutions.map((e) => e.id).sort(), + ).toEqual(['1', '3']); }); }); describe('lastSuccessfulExecution', () => { it('stores execution as id reference and resolves through executionData store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const exec = makeExecution({ id: 'last-1', status: 'success', finished: true }); - stateStore.setLastSuccessfulExecution(exec); + workflowExecutionStateStore.setLastSuccessfulExecution(exec); - expect(stateStore.lastSuccessfulExecutionId).toBe('last-1'); - expect(stateStore.lastSuccessfulExecution?.id).toBe('last-1'); + expect(workflowExecutionStateStore.lastSuccessfulExecutionId).toBe('last-1'); + expect(workflowExecutionStateStore.lastSuccessfulExecution?.id).toBe('last-1'); }); it('is independent of active/displayed execution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('active-1')).setExecution( makeExecution({ id: 'active-1' }), ); - stateStore.setActiveExecutionId('active-1'); - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); + workflowExecutionStateStore.setActiveExecutionId('active-1'); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); - expect(stateStore.activeExecutionId).toBe('active-1'); - expect(stateStore.lastSuccessfulExecutionId).toBe('last-1'); - expect(stateStore.activeExecution?.id).toBe('active-1'); - expect(stateStore.lastSuccessfulExecution?.id).toBe('last-1'); + expect(workflowExecutionStateStore.activeExecutionId).toBe('active-1'); + expect(workflowExecutionStateStore.lastSuccessfulExecutionId).toBe('last-1'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('active-1'); + expect(workflowExecutionStateStore.lastSuccessfulExecution?.id).toBe('last-1'); }); it('disposes the previous executionData store entry on replacement', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); const pinia = getActivePinia(); const previousStoreId = useExecutionDataStore(createExecutionDataId('last-1')).$id; - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-2' })); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-2' })); expect(pinia?.state.value[previousStoreId]).toBeUndefined(); - expect(stateStore.lastSuccessfulExecution?.id).toBe('last-2'); + expect(workflowExecutionStateStore.lastSuccessfulExecution?.id).toBe('last-2'); }); it('clears via null', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); - stateStore.setLastSuccessfulExecution(null); - expect(stateStore.lastSuccessfulExecutionId).toBeNull(); - expect(stateStore.lastSuccessfulExecution).toBeNull(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'last-1' })); + workflowExecutionStateStore.setLastSuccessfulExecution(null); + expect(workflowExecutionStateStore.lastSuccessfulExecutionId).toBeNull(); + expect(workflowExecutionStateStore.lastSuccessfulExecution).toBeNull(); }); it('does not dispose the previous store when it is also the active execution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('A')).setExecution(makeExecution({ id: 'A' })); - stateStore.setActiveExecutionId('A'); - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'A' })); + workflowExecutionStateStore.setActiveExecutionId('A'); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'A' })); const pinia = getActivePinia(); const aStoreId = useExecutionDataStore(createExecutionDataId('A')).$id; - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'B' })); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'B' })); expect(pinia?.state.value[aStoreId]).toBeDefined(); - expect(stateStore.activeExecution?.id).toBe('A'); - expect(stateStore.lastSuccessfulExecution?.id).toBe('B'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('A'); + expect(workflowExecutionStateStore.lastSuccessfulExecution?.id).toBe('B'); }); it('does not dispose the previous store when it is also the displayed execution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('A')).setExecution(makeExecution({ id: 'A' })); - stateStore.setActiveExecutionId('A'); - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'A' })); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('A'); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'A' })); + workflowExecutionStateStore.setActiveExecutionId(undefined); const pinia = getActivePinia(); const aStoreId = useExecutionDataStore(createExecutionDataId('A')).$id; - stateStore.setLastSuccessfulExecution(makeExecution({ id: 'B' })); + workflowExecutionStateStore.setLastSuccessfulExecution(makeExecution({ id: 'B' })); expect(pinia?.state.value[aStoreId]).toBeDefined(); - expect(stateStore.displayedExecutionId).toBe('A'); - expect(stateStore.activeExecution?.id).toBe('A'); - expect(stateStore.lastSuccessfulExecution?.id).toBe('B'); + expect(workflowExecutionStateStore.displayedExecutionId).toBe('A'); + expect(workflowExecutionStateStore.activeExecution?.id).toBe('A'); + expect(workflowExecutionStateStore.lastSuccessfulExecution?.id).toBe('B'); }); }); describe('chat + trigger + flags', () => { it('appendChatMessage / resetChatMessages round-trip', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.appendChatMessage('hello'); - stateStore.appendChatMessage('world'); - expect(stateStore.chatMessages).toEqual(['hello', 'world']); - expect(stateStore.getPastChatMessages).toEqual(['hello', 'world']); - stateStore.resetChatMessages(); - expect(stateStore.chatMessages).toEqual([]); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.appendChatMessage('hello'); + workflowExecutionStateStore.appendChatMessage('world'); + expect(workflowExecutionStateStore.chatMessages).toEqual(['hello', 'world']); + expect(workflowExecutionStateStore.getPastChatMessages).toEqual(['hello', 'world']); + workflowExecutionStateStore.resetChatMessages(); + expect(workflowExecutionStateStore.chatMessages).toEqual([]); }); it('setSelectedTriggerNodeName updates the value', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setSelectedTriggerNodeName('TriggerA'); - expect(stateStore.selectedTriggerNodeName).toBe('TriggerA'); - stateStore.setSelectedTriggerNodeName(undefined); - expect(stateStore.selectedTriggerNodeName).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setSelectedTriggerNodeName('TriggerA'); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBe('TriggerA'); + workflowExecutionStateStore.setSelectedTriggerNodeName(undefined); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBeUndefined(); }); it('setExecutionWaitingForWebhook / setIsInDebugMode toggle flags', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setExecutionWaitingForWebhook(true); - stateStore.setIsInDebugMode(true); - expect(stateStore.executionWaitingForWebhook).toBe(true); - expect(stateStore.isInDebugMode).toBe(true); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setExecutionWaitingForWebhook(true); + workflowExecutionStateStore.setIsInDebugMode(true); + expect(workflowExecutionStateStore.executionWaitingForWebhook).toBe(true); + expect(workflowExecutionStateStore.isInDebugMode).toBe(true); }); it('setChatPartialExecutionDestinationNode round-trip', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setChatPartialExecutionDestinationNode('NodeA'); - expect(stateStore.chatPartialExecutionDestinationNode).toBe('NodeA'); - stateStore.setChatPartialExecutionDestinationNode(null); - expect(stateStore.chatPartialExecutionDestinationNode).toBeNull(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode('NodeA'); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBe('NodeA'); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode(null); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBeNull(); }); }); describe('renameExecutionStateNode', () => { it('renames selectedTriggerNodeName and chatPartialExecutionDestinationNode', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setSelectedTriggerNodeName('Old'); - stateStore.setChatPartialExecutionDestinationNode('Old'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setSelectedTriggerNodeName('Old'); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode('Old'); - stateStore.renameExecutionStateNode('Old', 'New'); + workflowExecutionStateStore.renameExecutionStateNode('Old', 'New'); - expect(stateStore.selectedTriggerNodeName).toBe('New'); - expect(stateStore.chatPartialExecutionDestinationNode).toBe('New'); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBe('New'); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBe('New'); }); it('does nothing when names do not match', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setSelectedTriggerNodeName('A'); - stateStore.setChatPartialExecutionDestinationNode('B'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setSelectedTriggerNodeName('A'); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode('B'); - stateStore.renameExecutionStateNode('Other', 'Renamed'); + workflowExecutionStateStore.renameExecutionStateNode('Other', 'Renamed'); - expect(stateStore.selectedTriggerNodeName).toBe('A'); - expect(stateStore.chatPartialExecutionDestinationNode).toBe('B'); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBe('A'); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBe('B'); }); }); describe('renameActiveExecutionNode (cross-store)', () => { it('renames runData keys on the resolved executionData store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution( makeExecution({ @@ -786,9 +923,9 @@ describe('workflowExecutionState.store', () => { } as never, }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - stateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); + workflowExecutionStateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); const runData = executionDataStore.execution?.data?.resultData.runData; expect(runData?.New).toBeDefined(); @@ -796,14 +933,16 @@ describe('workflowExecutionState.store', () => { }); it('renames selectedTriggerNodeName and chatPartialExecutionDestinationNode', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setSelectedTriggerNodeName('Old'); - stateStore.setChatPartialExecutionDestinationNode('Old'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setSelectedTriggerNodeName('Old'); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode('Old'); - stateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); + workflowExecutionStateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); - expect(stateStore.selectedTriggerNodeName).toBe('New'); - expect(stateStore.chatPartialExecutionDestinationNode).toBe('New'); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBe('New'); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBe('New'); }); it('marks UI state dirty and remaps uiStore.lastSelectedNode when it matches', async () => { @@ -812,9 +951,11 @@ describe('workflowExecutionState.store', () => { uiStore.lastSelectedNode = 'Old'; const markDirtySpy = vi.spyOn(uiStore, 'markStateDirty'); - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); - stateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); + workflowExecutionStateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); expect(markDirtySpy).toHaveBeenCalled(); expect(uiStore.lastSelectedNode).toBe('New'); @@ -825,14 +966,18 @@ describe('workflowExecutionState.store', () => { const uiStore = useUIStore(); uiStore.lastSelectedNode = 'Untouched'; - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); expect(uiStore.lastSelectedNode).toBe('Untouched'); }); it('routes through the displayedExecutionId when active is cleared', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1')); executionDataStore.setExecution( makeExecution({ @@ -840,10 +985,10 @@ describe('workflowExecutionState.store', () => { data: { resultData: { runData: { Old: [] } } } as never, }), ); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId(undefined); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId(undefined); - stateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); + workflowExecutionStateStore.renameActiveExecutionNode({ old: 'Old', new: 'New' }); expect(executionDataStore.execution?.data?.resultData.runData.New).toBeDefined(); }); @@ -851,7 +996,9 @@ describe('workflowExecutionState.store', () => { describe('resolveExecutionTriggerNodeName', () => { it('returns triggerNode from active execution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', @@ -860,13 +1007,17 @@ describe('workflowExecutionState.store', () => { finished: false, }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.resolveExecutionTriggerNodeName(['TriggerA', 'TriggerB'])).toBe('TriggerA'); + expect( + workflowExecutionStateStore.resolveExecutionTriggerNodeName(['TriggerA', 'TriggerB']), + ).toBe('TriggerA'); }); it('falls back to runData keys for partial executions', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1', @@ -875,63 +1026,73 @@ describe('workflowExecutionState.store', () => { data: { resultData: { runData: { TriggerB: [] } } } as never, }), ); - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); - expect(stateStore.resolveExecutionTriggerNodeName(['TriggerA', 'TriggerB'])).toBe('TriggerB'); + expect( + workflowExecutionStateStore.resolveExecutionTriggerNodeName(['TriggerA', 'TriggerB']), + ).toBe('TriggerB'); }); it('returns undefined when not running', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - expect(stateStore.resolveExecutionTriggerNodeName(['TriggerA'])).toBeUndefined(); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + expect( + workflowExecutionStateStore.resolveExecutionTriggerNodeName(['TriggerA']), + ).toBeUndefined(); }); }); describe('resetExecutionState', () => { it('clears every state field', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setPendingExecution(makeExecution()); - stateStore.setExecutionWaitingForWebhook(true); - stateStore.setIsInDebugMode(true); - stateStore.appendChatMessage('hi'); - stateStore.setChatPartialExecutionDestinationNode('Node'); - stateStore.setSelectedTriggerNodeName('Trigger'); - stateStore.setCurrentWorkflowExecutions([makeExecutionSummary({ id: '1' })]); - stateStore.setLastSuccessfulExecutionId('last-1'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setPendingExecution(makeExecution()); + workflowExecutionStateStore.setExecutionWaitingForWebhook(true); + workflowExecutionStateStore.setIsInDebugMode(true); + workflowExecutionStateStore.appendChatMessage('hi'); + workflowExecutionStateStore.setChatPartialExecutionDestinationNode('Node'); + workflowExecutionStateStore.setSelectedTriggerNodeName('Trigger'); + workflowExecutionStateStore.setCurrentWorkflowExecutions([makeExecutionSummary({ id: '1' })]); + workflowExecutionStateStore.setLastSuccessfulExecutionId('last-1'); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); - expect(stateStore.activeExecutionId).toBeUndefined(); - expect(stateStore.displayedExecutionId).toBeUndefined(); - expect(stateStore.previousExecutionId).toBeUndefined(); - expect(stateStore.pendingExecution).toBeNull(); - expect(stateStore.executionWaitingForWebhook).toBe(false); - expect(stateStore.isInDebugMode).toBe(false); - expect(stateStore.chatMessages).toEqual([]); - expect(stateStore.chatPartialExecutionDestinationNode).toBeNull(); - expect(stateStore.selectedTriggerNodeName).toBeUndefined(); - expect(stateStore.currentWorkflowExecutions).toEqual([]); - expect(stateStore.lastSuccessfulExecutionId).toBeNull(); + expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.displayedExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.previousExecutionId).toBeUndefined(); + expect(workflowExecutionStateStore.pendingExecution).toBeNull(); + expect(workflowExecutionStateStore.executionWaitingForWebhook).toBe(false); + expect(workflowExecutionStateStore.isInDebugMode).toBe(false); + expect(workflowExecutionStateStore.chatMessages).toEqual([]); + expect(workflowExecutionStateStore.chatPartialExecutionDestinationNode).toBeNull(); + expect(workflowExecutionStateStore.selectedTriggerNodeName).toBeUndefined(); + expect(workflowExecutionStateStore.currentWorkflowExecutions).toEqual([]); + expect(workflowExecutionStateStore.lastSuccessfulExecutionId).toBeNull(); }); it('disposes per-execution data stores for every tracked id', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); // Three sequential runs — exec-1 rolls out of previousExecutionId after exec-3. - stateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), ); - stateStore.setActiveExecutionId('exec-2'); + workflowExecutionStateStore.setActiveExecutionId('exec-2'); useExecutionDataStore(createExecutionDataId('exec-2')).setExecution( makeExecution({ id: 'exec-2' }), ); - stateStore.setActiveExecutionId('exec-3'); + workflowExecutionStateStore.setActiveExecutionId('exec-3'); useExecutionDataStore(createExecutionDataId('exec-3')).setExecution( makeExecution({ id: 'exec-3' }), ); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); // All three stores must be disposed — even exec-1 which is in no slot. expect(useExecutionDataStore(createExecutionDataId('exec-1')).execution).toBeNull(); @@ -940,15 +1101,17 @@ describe('workflowExecutionState.store', () => { }); it('disposes the IN_PROGRESS placeholder store', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setPendingExecution( + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setPendingExecution( makeExecution({ id: IN_PROGRESS_EXECUTION_ID, status: 'running' }), ); useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution( makeExecution({ id: IN_PROGRESS_EXECUTION_ID }), ); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); expect( useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).execution, @@ -958,11 +1121,13 @@ describe('workflowExecutionState.store', () => { describe('trackExecutionId', () => { it('tracks ids written via setActiveExecutionId across rolling runs', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); - stateStore.setActiveExecutionId('exec-1'); - stateStore.setActiveExecutionId('exec-2'); - stateStore.setActiveExecutionId('exec-3'); + workflowExecutionStateStore.setActiveExecutionId('exec-1'); + workflowExecutionStateStore.setActiveExecutionId('exec-2'); + workflowExecutionStateStore.setActiveExecutionId('exec-3'); useExecutionDataStore(createExecutionDataId('exec-1')).setExecution( makeExecution({ id: 'exec-1' }), @@ -974,7 +1139,7 @@ describe('workflowExecutionState.store', () => { makeExecution({ id: 'exec-3' }), ); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); // All three are disposed even though exec-1 is in no slot after run 3. expect(useExecutionDataStore(createExecutionDataId('exec-1')).execution).toBeNull(); @@ -983,20 +1148,24 @@ describe('workflowExecutionState.store', () => { }); it('tracks ids written via setLastSuccessfulExecution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setLastSuccessfulExecution( + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setLastSuccessfulExecution( makeExecution({ id: 'exec-success', finished: true, status: 'success' }), ); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); expect(useExecutionDataStore(createExecutionDataId('exec-success')).execution).toBeNull(); }); it('does not track the IN_PROGRESS placeholder id (handled separately)', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.trackExecutionId(IN_PROGRESS_EXECUTION_ID); - stateStore.trackExecutionId('real-exec'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.trackExecutionId(IN_PROGRESS_EXECUTION_ID); + workflowExecutionStateStore.trackExecutionId('real-exec'); useExecutionDataStore(createExecutionDataId('real-exec')).setExecution( makeExecution({ id: 'real-exec' }), @@ -1005,7 +1174,7 @@ describe('workflowExecutionState.store', () => { makeExecution({ id: IN_PROGRESS_EXECUTION_ID }), ); - stateStore.resetExecutionState(); + workflowExecutionStateStore.resetExecutionState(); // Both stores get disposed: real-exec via tracked set, IN_PROGRESS unconditionally. expect(useExecutionDataStore(createExecutionDataId('real-exec')).execution).toBeNull(); @@ -1015,38 +1184,43 @@ describe('workflowExecutionState.store', () => { }); it('ignores null and undefined ids', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.trackExecutionId(null); - stateStore.trackExecutionId(undefined); - stateStore.trackExecutionId(''); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.trackExecutionId(null); + workflowExecutionStateStore.trackExecutionId(undefined); + workflowExecutionStateStore.trackExecutionId(''); // resetExecutionState must complete without error and produce no surprising side effects. - expect(() => stateStore.resetExecutionState()).not.toThrow(); + expect(() => workflowExecutionStateStore.resetExecutionState()).not.toThrow(); }); }); describe('event hook', () => { it('fires onWorkflowExecutionStateChange with workflowId and field discriminator', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); + const id = createWorkflowDocumentId('wf-1'); + const workflowExecutionStateStore = useWorkflowExecutionStateStore(id); const spy = vi.fn(); - stateStore.onWorkflowExecutionStateChange(spy); + workflowExecutionStateStore.onWorkflowExecutionStateChange(spy); - stateStore.setExecutionWaitingForWebhook(true); + workflowExecutionStateStore.setExecutionWaitingForWebhook(true); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0]).toMatchObject({ action: 'update', - payload: { workflowId: 'wf-1', field: 'executionWaitingForWebhook' }, + payload: { documentId: id, field: 'executionWaitingForWebhook' }, }); }); it('emits a delete action when clearing pending execution', () => { - const stateStore = useWorkflowExecutionStateStore(createWorkflowExecutionStateId('wf-1')); - stateStore.setPendingExecution(makeExecution()); + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId('wf-1'), + ); + workflowExecutionStateStore.setPendingExecution(makeExecution()); const spy = vi.fn(); - stateStore.onWorkflowExecutionStateChange(spy); - stateStore.setPendingExecution(null); + workflowExecutionStateStore.onWorkflowExecutionStateChange(spy); + workflowExecutionStateStore.setPendingExecution(null); expect(spy.mock.calls.some((c) => c[0].action === 'delete')).toBe(true); }); @@ -1054,21 +1228,23 @@ describe('workflowExecutionState.store', () => { describe('disposeWorkflowExecutionStateStore', () => { it('removes pinia state and recreate yields fresh state', () => { - const id = createWorkflowExecutionStateId('wf-disposable'); - const stateStore = useWorkflowExecutionStateStore(id); + const id = 'wf-disposable'; + const workflowExecutionStateStore = useWorkflowExecutionStateStore( + createWorkflowDocumentId(id), + ); const pinia = getActivePinia(); - const disposeSpy = vi.spyOn(stateStore, '$dispose'); + const disposeSpy = vi.spyOn(workflowExecutionStateStore, '$dispose'); - stateStore.setExecutionWaitingForWebhook(true); - expect(pinia?.state.value[stateStore.$id]).toBeDefined(); + workflowExecutionStateStore.setExecutionWaitingForWebhook(true); + expect(pinia?.state.value[workflowExecutionStateStore.$id]).toBeDefined(); - disposeWorkflowExecutionStateStore(stateStore); + disposeWorkflowExecutionStateStore(workflowExecutionStateStore); expect(disposeSpy).toHaveBeenCalledOnce(); - expect(pinia?.state.value[stateStore.$id]).toBeUndefined(); + expect(pinia?.state.value[workflowExecutionStateStore.$id]).toBeUndefined(); - const recreated = useWorkflowExecutionStateStore(id); - expect(recreated).not.toBe(stateStore); + const recreated = useWorkflowExecutionStateStore(createWorkflowDocumentId(id)); + expect(recreated).not.toBe(workflowExecutionStateStore); expect(recreated.executionWaitingForWebhook).toBe(false); }); }); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.ts b/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.ts index 715b9ae444e..b8266ed0ae8 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowExecutionState.store.ts @@ -13,16 +13,14 @@ import { disposeExecutionDataStore, useExecutionDataStore, } from './executionData.store'; -import { createWorkflowDocumentId, useWorkflowDocumentStore } from './workflowDocument.store'; +import { useWorkflowDocumentStore, type WorkflowDocumentId } from './workflowDocument.store'; import { CHANGE_ACTION } from './workflowDocument/types'; import type { ChangeAction, ChangeEvent } from './workflowDocument/types'; const EMPTY_EXECUTION_ISSUES_BY_NODE_NAME = new Map>(); -export type WorkflowExecutionStateId = string; - export type WorkflowExecutionStateChangePayload = { - workflowId: WorkflowExecutionStateId; + documentId: WorkflowDocumentId; field: WorkflowExecutionStateField; }; @@ -42,19 +40,18 @@ export type WorkflowExecutionStateField = export type WorkflowExecutionStateChangeEvent = ChangeEvent; -export function createWorkflowExecutionStateId(workflowId: string): WorkflowExecutionStateId { - return workflowId; -} - /** * Gets the Pinia store id for a workflow-execution-state store. */ -export function getWorkflowExecutionStateStoreId(id: WorkflowExecutionStateId) { +export function getWorkflowExecutionStateStoreId(id: WorkflowDocumentId) { return `${STORES.WORKFLOW_EXECUTION_STATES}/${id}`; } /** - * Creates a workflow-execution-state store keyed by workflow id. + * Creates a workflow-execution-state store keyed by the workflow document id. + * One execution-state store exists per workflow-document store, so the two + * share an identity — pass the same `WorkflowDocumentId` (constructed via + * `createWorkflowDocumentId`) to both factories. * * Owns per-workflow execution UI state — active/displayed/previous * execution ids, the pending-execution scaffold, chat, debug, webhook wait, @@ -62,9 +59,10 @@ export function getWorkflowExecutionStateStoreId(id: WorkflowExecutionStateId) { * reference. Reads route through `useExecutionDataStore` for execution payloads * (or fall back to `pendingExecution` while `activeExecutionId === null`). */ -export function useWorkflowExecutionStateStore(id: WorkflowExecutionStateId) { +export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) { return defineStore(getWorkflowExecutionStateStoreId(id), () => { - const workflowId = id; + const documentId = id; + const [workflowId] = id.split('@'); // --- State --- @@ -104,7 +102,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowExecutionStateId) { function fireChange(action: ChangeAction, field: WorkflowExecutionStateField) { void onWorkflowExecutionStateChange.trigger({ action, - payload: { workflowId, field }, + payload: { documentId, field }, }); } @@ -561,9 +559,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowExecutionStateId) { } if (workflowId) { - const workflowDocumentStore = useWorkflowDocumentStore( - createWorkflowDocumentId(workflowId), - ); + const workflowDocumentStore = useWorkflowDocumentStore(documentId); workflowDocumentStore.renameNodeMetadata(nameData.old, nameData.new); workflowDocumentStore.renamePinDataNode(nameData.old, nameData.new); } @@ -595,6 +591,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowExecutionStateId) { } return { + documentId, workflowId, // Read API activeExecutionId: readonly(activeExecutionId), diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts index 248ab21acef..3e07e261015 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts @@ -1261,30 +1261,6 @@ describe('useWorkflowsStore', () => { }); describe('execution session setters', () => { - it('setExecutionWaitingForWebhook updates the value', () => { - expect(workflowsStore.executionWaitingForWebhook).toBe(false); - workflowsStore.setExecutionWaitingForWebhook(true); - expect(workflowsStore.executionWaitingForWebhook).toBe(true); - workflowsStore.setExecutionWaitingForWebhook(false); - expect(workflowsStore.executionWaitingForWebhook).toBe(false); - }); - - it('setIsInDebugMode updates the value', () => { - expect(workflowsStore.isInDebugMode).toBe(false); - workflowsStore.setIsInDebugMode(true); - expect(workflowsStore.isInDebugMode).toBe(true); - workflowsStore.setIsInDebugMode(false); - expect(workflowsStore.isInDebugMode).toBe(false); - }); - - it('setChatPartialExecutionDestinationNode updates the value', () => { - expect(workflowsStore.chatPartialExecutionDestinationNode).toBeNull(); - workflowsStore.setChatPartialExecutionDestinationNode('Some Node'); - expect(workflowsStore.chatPartialExecutionDestinationNode).toBe('Some Node'); - workflowsStore.setChatPartialExecutionDestinationNode(null); - expect(workflowsStore.chatPartialExecutionDestinationNode).toBeNull(); - }); - it('setLastSuccessfulExecution updates the value independently of active execution', () => { const execution = { id: 'last-success' } as IExecutionResponse; workflowsStore.setWorkflowExecutionData({ @@ -1377,34 +1353,4 @@ describe('useWorkflowsStore', () => { expect(workflowsStore.currentWorkflowExecutions).toEqual([exec2]); }); }); - - describe('activeExecutionId tri-state', () => { - it('starts undefined (not tracking)', () => { - expect(workflowsStore.activeExecutionId).toBeUndefined(); - }); - - it('null indicates execution started but id pending', () => { - workflowsStore.private.setActiveExecutionId(null); - expect(workflowsStore.activeExecutionId).toBeNull(); - }); - - it('string indicates known execution id', () => { - workflowsStore.private.setActiveExecutionId('exec-1'); - expect(workflowsStore.activeExecutionId).toBe('exec-1'); - }); - - it('rolls activeExecutionId into previousExecutionId on transition to a new id', () => { - workflowsStore.private.setActiveExecutionId('exec-1'); - workflowsStore.private.setActiveExecutionId('exec-2'); - expect(workflowsStore.previousExecutionId).toBe('exec-1'); - expect(workflowsStore.activeExecutionId).toBe('exec-2'); - }); - - it('does not update previousExecutionId when clearing to undefined', () => { - workflowsStore.private.setActiveExecutionId('exec-1'); - workflowsStore.private.setActiveExecutionId(undefined); - expect(workflowsStore.previousExecutionId).toBeUndefined(); - expect(workflowsStore.activeExecutionId).toBeUndefined(); - }); - }); }); 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 523b71a8d3f..467bfac2cea 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -48,10 +48,7 @@ import { useWorkflowDocumentStore, createWorkflowDocumentId, } from '@/app/stores/workflowDocument.store'; -import { - createWorkflowExecutionStateId, - useWorkflowExecutionStateStore, -} from '@/app/stores/workflowExecutionState.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const uiStore = useUIStore(); @@ -83,7 +80,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { // done together to preserve test contracts. const currentExecutionStateStore = computed(() => - useWorkflowExecutionStateStore(createWorkflowExecutionStateId(workflowId.value)), + useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowId.value)), ); const currentWorkflowExecutions = computed({ @@ -118,30 +115,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { >, ); - const executionWaitingForWebhook = computed({ - get: () => currentExecutionStateStore.value.executionWaitingForWebhook, - set: (value) => currentExecutionStateStore.value.setExecutionWaitingForWebhook(value), - }); - - const isInDebugMode = computed({ - get: () => currentExecutionStateStore.value.isInDebugMode, - set: (value) => currentExecutionStateStore.value.setIsInDebugMode(value), - }); - - const chatMessages = computed( - () => currentExecutionStateStore.value.chatMessages as string[], - ); - - const chatPartialExecutionDestinationNode = computed({ - get: () => currentExecutionStateStore.value.chatPartialExecutionDestinationNode, - set: (value) => currentExecutionStateStore.value.setChatPartialExecutionDestinationNode(value), - }); - - const selectedTriggerNodeName = computed({ - get: () => currentExecutionStateStore.value.selectedTriggerNodeName, - set: (value) => currentExecutionStateStore.value.setSelectedTriggerNodeName(value), - }); - // A workflow is new if it hasn't been saved to the backend yet. // TODO: move to workflowDocumentStore after `workflow` ref is removed from this store. // When moved, preserve the `workflowsListStore.getWorkflowById` coupling — pure @@ -174,21 +147,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { return workflowExecutionData.value.data.resultData.runData; }); - const isWorkflowRunning = computed(() => { - if (activeExecutionId.value === null) { - return true; - } else if (activeExecutionId.value && workflowExecutionData.value) { - if ( - ['waiting', 'running'].includes(workflowExecutionData.value.status) && - !workflowExecutionData.value.finished - ) { - return true; - } - } - - return false; - }); - const executedNode = computed(() => workflowExecutionData.value?.executedNode); const getAllLoadedFinishedExecutions = computed(() => { @@ -199,37 +157,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const getWorkflowExecution = computed(() => workflowExecutionData.value); - const getPastChatMessages = computed(() => chatMessages.value); - const canViewWorkflows = computed( () => !settingsStore.isChatFeatureEnabled || !hasRole(['global:chatUser']), ); - /** - * Active execution id (tri-state): - * - undefined → no active execution being tracked - * - null → execution started but backend id not yet known - * - string → active backend execution id - * - * Routes through the per-workflow session store. Exposed as a writable - * computed so existing test setups that mutate via `createTestingPinia` - * continue to work. - */ - const activeExecutionId = computed({ - get: () => currentExecutionStateStore.value.activeExecutionId, - set: (value) => currentExecutionStateStore.value.setActiveExecutionId(value), - }); - const readonlyActiveExecutionId = computed( - () => currentExecutionStateStore.value.activeExecutionId, - ); - const readonlyPreviousExecutionId = computed( - () => currentExecutionStateStore.value.previousExecutionId, - ); - - function setActiveExecutionId(id: string | null | undefined) { - currentExecutionStateStore.value.setActiveExecutionId(id); - } - function getWorkflowResultDataByNodeName(nodeName: string): ITaskData[] | null { if (getWorkflowRunData.value === null) { return null; @@ -420,18 +351,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { currentExecutionStateStore.value.clearActiveExecutionStartedData(); } - function setExecutionWaitingForWebhook(value: boolean): void { - currentExecutionStateStore.value.setExecutionWaitingForWebhook(value); - } - - function setIsInDebugMode(value: boolean): void { - currentExecutionStateStore.value.setIsInDebugMode(value); - } - - function setChatPartialExecutionDestinationNode(value: string | null): void { - currentExecutionStateStore.value.setChatPartialExecutionDestinationNode(value); - } - function setLastSuccessfulExecution(execution: IExecutionResponse | null): void { currentExecutionStateStore.value.setLastSuccessfulExecution(execution); } @@ -731,41 +650,20 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { return url.toString(); } - function resetChatMessages(): void { - currentExecutionStateStore.value.resetChatMessages(); - } - - function appendChatMessage(message: string): void { - currentExecutionStateStore.value.appendChatMessage(message); - } - - function setSelectedTriggerNodeName(value: string | undefined) { - currentExecutionStateStore.value.setSelectedTriggerNodeName(value); - } - return { currentWorkflowExecutions, workflowExecutionData, workflowExecutionPairedItemMappings, workflowExecutionResultDataLastUpdate, workflowExecutionStartedData, - activeExecutionId: readonlyActiveExecutionId, - previousExecutionId: readonlyPreviousExecutionId, - executionWaitingForWebhook, - isInDebugMode, - chatMessages, - chatPartialExecutionDestinationNode, workflowId, isNewWorkflow, isWorkflowSaved, getWorkflowRunData, getWorkflowResultDataByNodeName, - isWorkflowRunning, executedNode, getAllLoadedFinishedExecutions, getWorkflowExecution, - getPastChatMessages, - selectedTriggerNodeName, getExecutionDataById, convertTemplateNodeToNodeUi, getWorkflowFromUrl, @@ -782,9 +680,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { setWorkflowExecutionRunData, setWorkflowExecutionData, clearExecutionStartedData, - setExecutionWaitingForWebhook, - setIsInDebugMode, - setChatPartialExecutionDestinationNode, setLastSuccessfulExecution, clearCurrentWorkflowExecutions, setCurrentWorkflowExecutions, @@ -803,17 +698,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { deleteExecution, addToCurrentExecutions, getBinaryUrl, - resetChatMessages, - appendChatMessage, getPartialIdForNode, - setSelectedTriggerNodeName, fetchLastSuccessfulExecution, lastSuccessfulExecution, canViewWorkflows, - // This is exposed to ease the refactoring to the injected workflowState composable - // Please do not use outside this context - private: { - setActiveExecutionId, - }, }; }); diff --git a/packages/frontend/editor-ui/src/app/views/NodeView.test.ts b/packages/frontend/editor-ui/src/app/views/NodeView.test.ts index 392c731b5f7..d7a2798a20c 100644 --- a/packages/frontend/editor-ui/src/app/views/NodeView.test.ts +++ b/packages/frontend/editor-ui/src/app/views/NodeView.test.ts @@ -7,6 +7,7 @@ import { } from '../stores/workflowDocument.store'; import { createPinia, setActivePinia } from 'pinia'; import { useWorkflowsStore } from '../stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '../stores/workflowExecutionState.store'; import { useNodeTypesStore } from '../stores/nodeTypes.store'; import { renderComponent } from '@/__tests__/render'; import NodeView from './NodeView.vue'; @@ -31,11 +32,14 @@ vi.mock('vue-router', () => ({ describe('NodeView', () => { let workflowsStore: ReturnType; let workflowDocumentStore: ReturnType; + let workflowExecutionState: ReturnType; beforeEach(() => { setActivePinia(createPinia()); workflowsStore = useWorkflowsStore(); + workflowsStore.setWorkflowId('w0'); workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('w0')); + workflowExecutionState = useWorkflowExecutionStateStore(createWorkflowDocumentId('w0')); }); describe('Trigger node selection', () => { @@ -75,18 +79,18 @@ describe('NodeView', () => { it('should select newly added trigger node automatically', async () => { renderNodeView(); - await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n0')); + await waitFor(() => expect(workflowExecutionState.selectedTriggerNodeName).toBe('n0')); workflowDocumentStore.addNode(n2); - await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n2')); + await waitFor(() => expect(workflowExecutionState.selectedTriggerNodeName).toBe('n2')); }); it('should re-select a trigger when selected trigger gets disabled or removed', async () => { renderNodeView(); - await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n0')); + await waitFor(() => expect(workflowExecutionState.selectedTriggerNodeName).toBe('n0')); useWorkflowDocumentStore( createWorkflowDocumentId(workflowDocumentStore.workflowId), ).removeNode(n0); - await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n1')); + await waitFor(() => expect(workflowExecutionState.selectedTriggerNodeName).toBe('n1')); useWorkflowDocumentStore( createWorkflowDocumentId(workflowDocumentStore.workflowId), ).setNodeValue({ @@ -94,7 +98,7 @@ describe('NodeView', () => { key: 'disabled', value: true, }); - await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe(undefined)); + await waitFor(() => expect(workflowExecutionState.selectedTriggerNodeName).toBe(undefined)); }); }); }); diff --git a/packages/frontend/editor-ui/src/app/views/NodeView.vue b/packages/frontend/editor-ui/src/app/views/NodeView.vue index 55fc7bb2711..a36a944fb87 100644 --- a/packages/frontend/editor-ui/src/app/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/app/views/NodeView.vue @@ -21,6 +21,7 @@ import { useUIStore } from '@/app/stores/ui.store'; import CanvasRunWorkflowButton from '@/features/workflows/canvas/components/elements/buttons/CanvasRunWorkflowButton.vue'; import { useI18n } from '@n8n/i18n'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import { useRunWorkflow } from '@/app/composables/useRunWorkflow'; import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions'; @@ -183,6 +184,10 @@ const clipboard = useClipboard({ onPaste: onClipboardPaste }); const nodeTypesStore = useNodeTypesStore(); const uiStore = useUIStore(); const workflowsStore = useWorkflowsStore(); +const workflowDocumentStore = injectWorkflowDocumentStore(); +const workflowExecutionState = computed(() => + useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId), +); const workflowsListStore = useWorkflowsListStore(); const sourceControlStore = useSourceControlStore(); const nodeCreatorStore = useNodeCreatorStore(); @@ -266,7 +271,6 @@ const readOnlyNotification = ref(null); const fallbackNodes = ref([]); const workflowId = useInjectWorkflowId(); -const workflowDocumentStore = injectWorkflowDocumentStore(); const routeNodeId = computed(() => { const nodeId = route.params.nodeId; return Array.isArray(nodeId) ? nodeId[0] : nodeId; @@ -426,7 +430,8 @@ const selectableTriggerNodes = computed(() => ); const isRunButtonSplit = computed(() => { return ( - selectableTriggerNodes.value.length > 1 && workflowsStore.selectedTriggerNodeName !== undefined + selectableTriggerNodes.value.length > 1 && + workflowExecutionState.value.selectedTriggerNodeName !== undefined ); }); @@ -1060,8 +1065,10 @@ const projectPermissions = computed(() => { const isStoppingExecution = ref(false); -const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning); -const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook); +const isWorkflowRunning = computed(() => workflowExecutionState.value.isWorkflowRunning); +const isExecutionWaitingForWebhook = computed( + () => workflowExecutionState.value.executionWaitingForWebhook, +); const isExecutionDisabled = computed(() => { if ( @@ -1734,15 +1741,17 @@ watch( [selectableTriggerNodes, workflowExecutionTriggerNodeName], ([newSelectable, currentTrigger], [oldSelectable]) => { if (currentTrigger !== undefined) { - workflowsStore.setSelectedTriggerNodeName(currentTrigger); + workflowExecutionState.value.setSelectedTriggerNodeName(currentTrigger); return; } if ( - workflowsStore.selectedTriggerNodeName === undefined || - newSelectable.every((node) => node.name !== workflowsStore.selectedTriggerNodeName) + workflowExecutionState.value.selectedTriggerNodeName === undefined || + newSelectable.every( + (node) => node.name !== workflowExecutionState.value.selectedTriggerNodeName, + ) ) { - workflowsStore.setSelectedTriggerNodeName( + workflowExecutionState.value.setSelectedTriggerNodeName( findTriggerNodeToAutoSelect(selectableTriggerNodes.value, nodeTypesStore.getNodeType)?.name, ); return; @@ -1754,7 +1763,7 @@ watch( if (newTrigger !== undefined) { // Select newly added node - workflowsStore.setSelectedTriggerNodeName(newTrigger.name); + workflowExecutionState.value.setSelectedTriggerNodeName(newTrigger.name); } }, { immediate: true }, @@ -1946,12 +1955,12 @@ onBeforeUnmount(() => { :executing="isWorkflowRunning" :trigger-nodes="triggerNodes" :get-node-type="nodeTypesStore.getNodeType" - :selected-trigger-node-name="workflowsStore.selectedTriggerNodeName" + :selected-trigger-node-name="workflowExecutionState.selectedTriggerNodeName" :embedded="isDemoRoute" @mouseenter="onRunWorkflowButtonMouseEnter" @mouseleave="onRunWorkflowButtonMouseLeave" @execute="runEntireWorkflow('main')" - @select-trigger-node="workflowsStore.setSelectedTriggerNodeName" + @select-trigger-node="workflowExecutionState.setSelectedTriggerNodeName" />