refactor(editor): Move session state off workflows-store bridge (no-changelog) (#31138)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Grozav 2026-05-28 14:51:24 +03:00 committed by GitHub
parent e104c7f299
commit 1eee149ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1128 additions and 842 deletions

View File

@ -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: () => ({}),

View File

@ -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);

View File

@ -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();

View File

@ -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) {}

View File

@ -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' }));

View File

@ -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');
}

View File

@ -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<WorkflowState>({

View File

@ -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,

View File

@ -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<typeof useRouter>; 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;
}

View File

@ -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<typeof useWorkflowsStore>;
let stateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
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');
});
});
});

View File

@ -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,

View File

@ -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<WorkflowState> };
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let stateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
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<WorkflowState>({

View File

@ -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,

View File

@ -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<typeof useWorkflowsStore>;
let stateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
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 () => {

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<T> = { -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<string, unknown> = {
@ -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);

View File

@ -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<IExecutionPushResponse | undefined> {
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.

View File

@ -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);
}
}
}

View File

@ -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<typeof useWorkflowsStore>;
let workflowState: WorkflowState;
let stateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
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();

View File

@ -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();

View File

@ -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<string, ComputedRef<CanvasConnectionPort[]>>());
@ -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);
});
});

View File

@ -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,

View File

@ -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<string, ComputedRef<string[]>>();
export type WorkflowExecutionStateId = string;
export type WorkflowExecutionStateChangePayload = {
workflowId: WorkflowExecutionStateId;
documentId: WorkflowDocumentId;
field: WorkflowExecutionStateField;
};
@ -42,19 +40,18 @@ export type WorkflowExecutionStateField =
export type WorkflowExecutionStateChangeEvent = ChangeEvent<WorkflowExecutionStateChangePayload>;
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),

View File

@ -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();
});
});
});

View File

@ -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<ExecutionSummary[]>({
@ -118,30 +115,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
>,
);
const executionWaitingForWebhook = computed<boolean>({
get: () => currentExecutionStateStore.value.executionWaitingForWebhook,
set: (value) => currentExecutionStateStore.value.setExecutionWaitingForWebhook(value),
});
const isInDebugMode = computed<boolean>({
get: () => currentExecutionStateStore.value.isInDebugMode,
set: (value) => currentExecutionStateStore.value.setIsInDebugMode(value),
});
const chatMessages = computed<string[]>(
() => currentExecutionStateStore.value.chatMessages as string[],
);
const chatPartialExecutionDestinationNode = computed<string | null>({
get: () => currentExecutionStateStore.value.chatPartialExecutionDestinationNode,
set: (value) => currentExecutionStateStore.value.setChatPartialExecutionDestinationNode(value),
});
const selectedTriggerNodeName = computed<string | undefined>({
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<string | null | undefined>({
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,
},
};
});

View File

@ -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<typeof useWorkflowsStore>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
let workflowExecutionState: ReturnType<typeof useWorkflowExecutionStateStore>;
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));
});
});
});

View File

@ -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 | { visible: boolean }>(null);
const fallbackNodes = ref<INodeUi[]>([]);
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"
/>
<template v-if="containsChatTriggerNodes">
<CanvasChatButton

View File

@ -19,6 +19,7 @@ import { useRoute } from 'vue-router';
import { useSettingsStore } from '@/app/stores/settings.store';
import { assert } from '@n8n/utils/assert';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import type { ICredentialType, NodeError, INode } from 'n8n-workflow';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/app/composables/useTelemetry';
@ -197,7 +198,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
return (
chatSessionTask.value === 'error' &&
workflowsStore.activeExecutionId === currentSessionActiveExecutionId.value &&
useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowsStore.workflowId))
.activeExecutionId === currentSessionActiveExecutionId.value &&
targetNode === chatSessionError.value?.node.name
);
}
@ -492,8 +494,11 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
chatSessionError.value = context;
currentSessionWorkflowId.value = workflowId;
if (workflowsStore.activeExecutionId) {
currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId;
const activeExecutionId = useWorkflowExecutionStateStore(
createWorkflowDocumentId(workflowsStore.workflowId),
).activeExecutionId;
if (activeExecutionId) {
currentSessionActiveExecutionId.value = activeExecutionId;
}
const { authType, nodeInputData, schemas } = assistantHelpers.getNodeInfoForAssistant(

View File

@ -11,6 +11,7 @@ import { useInjectWorkflowId } from '@/app/composables/useInjectWorkflowId';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
@ -533,7 +534,9 @@ async function onExecuteWithMockData() {
});
await runWorkflow({
triggerNode: workflowsStore.selectedTriggerNodeName ?? triggerNode?.name,
triggerNode:
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId)
.selectedTriggerNodeName ?? triggerNode?.name,
});
}

View File

@ -11,6 +11,7 @@ import type { INodeUi } from '@/Interface';
import ExecuteMessage from './ExecuteMessage.vue';
import { CHAT_TRIGGER_NODE_TYPE, SETUP_CREDENTIALS_MODAL_KEY } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
@ -136,16 +137,19 @@ describe('ExecuteMessage', () => {
Object.defineProperty(workflowsStore, 'workflowExecutionData', {
get: () => workflowExecutionDataRef,
});
Object.defineProperty(workflowsStore, 'executionWaitingForWebhook', {
const workflowExecutionState = useWorkflowExecutionStateStore(
createWorkflowDocumentId('test-workflow'),
);
Object.defineProperty(workflowExecutionState, 'executionWaitingForWebhook', {
get: () => executionWaitingForWebhookRef.value,
set: (value: boolean) => {
executionWaitingForWebhookRef.value = value;
},
});
Object.defineProperty(workflowsStore, 'selectedTriggerNodeName', {
Object.defineProperty(workflowExecutionState, 'selectedTriggerNodeName', {
get: () => selectedTriggerNodeNameRef.value,
});
workflowsStore.setSelectedTriggerNodeName = vi.fn((name: string | undefined) => {
workflowExecutionState.setSelectedTriggerNodeName = vi.fn((name: string | undefined) => {
selectedTriggerNodeNameRef.value = name;
});
logsStore.toggleOpen = vi.fn();

View File

@ -1,6 +1,6 @@
<!-- eslint-disable import-x/extensions -->
<script setup lang="ts">
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
@ -33,11 +33,13 @@ interface Emits {
const emit = defineEmits<Emits>();
const workflowsStore = useWorkflowsStore();
const workflowId = useInjectWorkflowId();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowId.value)),
);
const workflowExecutionState = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const i18n = useI18n();
@ -304,9 +306,9 @@ watch(hasValidationIssues, (hasIssues, hadIssues) => {
size="medium"
:trigger-nodes="availableTriggerNodes"
:get-node-type="nodeTypesStore.getNodeType"
:selected-trigger-node-name="workflowsStore.selectedTriggerNodeName"
:selected-trigger-node-name="workflowExecutionState.selectedTriggerNodeName"
@execute="onExecute"
@select-trigger-node="workflowsStore.setSelectedTriggerNodeName"
@select-trigger-node="workflowExecutionState.setSelectedTriggerNodeName"
/>
</N8nTooltip>

View File

@ -3,6 +3,7 @@ import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useLogsStore } from '@/app/stores/logs.store';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
@ -23,6 +24,9 @@ export function useBuilderExecution(isReady: ComputedRef<boolean>) {
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionState = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const nodeTypesStore = useNodeTypesStore();
const logsStore = useLogsStore();
const toast = useToast();
@ -40,8 +44,10 @@ export function useBuilderExecution(isReady: ComputedRef<boolean>) {
!isReady.value ? i18n.baseText('aiAssistant.builder.executeMessage.validationTooltip') : '',
);
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
const isWorkflowRunning = computed(() => workflowExecutionState.value.isWorkflowRunning);
const isExecutionWaitingForWebhook = computed(
() => workflowExecutionState.value.executionWaitingForWebhook,
);
// --- Execution watcher ---
let executionWatcherStop: (() => void) | undefined;
@ -78,7 +84,7 @@ export function useBuilderExecution(isReady: ComputedRef<boolean>) {
if (!isReady.value) return false;
const selectedTriggerNode =
workflowsStore.selectedTriggerNodeName ?? availableTriggerNodes.value[0]?.name;
workflowExecutionState.value.selectedTriggerNodeName ?? availableTriggerNodes.value[0]?.name;
const selectedTriggerNodeType = selectedTriggerNode
? workflowDocumentStore.value?.getNodeByName(selectedTriggerNode)
: null;

View File

@ -38,6 +38,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
@ -583,7 +584,9 @@ export const useChatStore = defineStore(STORES.CHAT_HUB, () => {
});
// Signal canvas that an execution is pending (null = waiting for execution ID)
workflowsStore.private.setActiveExecutionId(null);
useWorkflowExecutionStateStore(
createWorkflowDocumentId(workflowsStore.workflowId),
).setActiveExecutionId(null);
}
async function sendMessage(

View File

@ -4,10 +4,8 @@ import type { InstanceAiAgentNode } from '@n8n/api-types';
import WorkflowCanvasHost from '@/app/components/WorkflowCanvasHost.vue';
import { EditorExternalReadOnlyKey } from '@/app/constants/injectionKeys';
import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
import {
createWorkflowExecutionStateId,
useWorkflowExecutionStateStore,
} from '@/app/stores/workflowExecutionState.store';
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
import type { FixWithAiError } from '../fixWithAi';
import { useThread } from '../instanceAi.store';
@ -59,9 +57,9 @@ const pushStore = usePushConnectionStore();
const removeExecutionStartedListener = pushStore.addEventListener((event) => {
if (event.type !== 'executionStarted') return;
if (event.data.workflowId !== props.workflowId) return;
useWorkflowExecutionStateStore(
createWorkflowExecutionStateId(props.workflowId),
).setActiveExecutionId(null);
useWorkflowExecutionStateStore(createWorkflowDocumentId(props.workflowId)).setActiveExecutionId(
null,
);
});
// On executionFinished with errors, surface a structured failures report so

View File

@ -8,7 +8,11 @@ import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/app/constants'
import { DEBUG_PAYWALL_MODAL_KEY } from '../executions.constants';
import type { INodeUi } from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
createWorkflowDocumentId,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useTelemetry } from '@/app/composables/useTelemetry';
@ -175,7 +179,9 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) =>
event.stopPropagation();
return;
}
workflowsStore.setIsInDebugMode(false);
useWorkflowExecutionStateStore(
createWorkflowDocumentId(workflowsStore.workflowId),
).setIsInDebugMode(false);
};
return {

View File

@ -44,6 +44,12 @@ vi.mock('@n8n/stores/useRootStore', () => ({
vi.mock('@/app/stores/workflows.store', () => ({
useWorkflowsStore: () => ({
workflowId: 'test-workflow',
}),
}));
vi.mock('@/app/stores/workflowExecutionState.store', () => ({
useWorkflowExecutionStateStore: () => ({
activeExecutionId: '123',
}),
}));

View File

@ -36,6 +36,8 @@ import {
WORKFLOW_TRIGGER_NODE_TYPE,
} from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { i18n } from '@n8n/i18n';
import { h } from 'vue';
@ -196,7 +198,9 @@ export const waitingNodeTooltip = (
node.type,
)?.waitingNodeTooltip;
if (waitingNodeTooltipFromNodeType) {
const activeExecutionId = useWorkflowsStore().activeExecutionId as string;
const activeExecutionId = useWorkflowExecutionStateStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
).activeExecutionId as string;
// Use signed URLs from metadata if available
// otherwise fall back to constructing URLs without token
const additionalData: IWorkflowDataProxyAdditionalKeys = {

View File

@ -4,6 +4,7 @@ import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { useChatState } from './useChatState';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { useLogsStore } from '@/app/stores/logs.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
@ -384,8 +385,11 @@ describe('useChatState', () => {
expect(chatState.webhookRegistered.value).toBe(true);
});
it('should include destinationNode when set in workflowsStore', async () => {
workflowsStore.setChatPartialExecutionDestinationNode('DestinationNode');
it('should include destinationNode when set in workflowExecutionState', async () => {
const executionStateStore = useWorkflowExecutionStateStore(
createWorkflowDocumentId('workflow-123'),
);
executionStateStore.setChatPartialExecutionDestinationNode('DestinationNode');
const chatState = useChatState(false);
await chatState.registerChatWebhook();
@ -399,7 +403,7 @@ describe('useChatState', () => {
mode: 'inclusive',
},
});
expect(workflowsStore.chatPartialExecutionDestinationNode).toBeNull();
expect(executionStateStore.chatPartialExecutionDestinationNode).toBeNull();
});
it('should not register if already registering', async () => {
@ -508,12 +512,15 @@ describe('useChatState', () => {
});
it('should clear partial execution destination node', () => {
workflowsStore.setChatPartialExecutionDestinationNode('SomeNode');
const executionStateStore = useWorkflowExecutionStateStore(
createWorkflowDocumentId('workflow-123'),
);
executionStateStore.setChatPartialExecutionDestinationNode('SomeNode');
const chatState = useChatState(false);
chatState.refreshSession();
expect(workflowsStore.chatPartialExecutionDestinationNode).toBeNull();
expect(executionStateStore.chatPartialExecutionDestinationNode).toBeNull();
});
});

View File

@ -3,6 +3,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { VIEWS } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import MessageWithButtons from '@n8n/chat/components/MessageWithButtons.vue';
@ -45,6 +46,9 @@ export function useChatState(
const locale = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionState = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const workflowState = injectWorkflowState();
const rootStore = useRootStore();
const logsStore = useLogsStore();
@ -62,7 +66,7 @@ export function useChatState(
// Use provided sessionId or fall back to logsStore sessionId
const effectiveSessionId = computed(() => toValue(sessionId) ?? currentSessionId.value);
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
const previousChatMessages = computed(() => workflowExecutionState.value.getPastChatMessages);
const chatTriggerNode = computed(
() => workflowDocumentStore.value.allNodes.find(isChatNode) ?? null,
);
@ -206,13 +210,13 @@ export function useChatState(
sessionId: effectiveSessionId.value,
};
if (workflowsStore.chatPartialExecutionDestinationNode) {
if (workflowExecutionState.value.chatPartialExecutionDestinationNode) {
runWorkflowOptions.destinationNode = {
nodeName: workflowsStore.chatPartialExecutionDestinationNode,
nodeName: workflowExecutionState.value.chatPartialExecutionDestinationNode,
mode: 'inclusive',
};
// Clear after use so subsequent messages run full workflow
workflowsStore.setChatPartialExecutionDestinationNode(null);
workflowExecutionState.value.setChatPartialExecutionDestinationNode(null);
}
const response = await runWorkflow(runWorkflowOptions);
@ -332,7 +336,7 @@ export function useChatState(
logsStore.resetChatSessionId();
logsStore.resetMessages();
// Clear partial execution destination to allow full workflow execution
workflowsStore.setChatPartialExecutionDestinationNode(null);
workflowExecutionState.value.setChatPartialExecutionDestinationNode(null);
if (logsStore.isOpen) {
chatEventBus.emit('focusInput');

View File

@ -4,14 +4,18 @@ import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
export function useClearExecutionButtonVisible() {
const route = useRoute();
const sourceControlStore = useSourceControlStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const isWorkflowRunning = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
const nodeTypesStore = useNodeTypesStore();
const isReadOnlyEnvironment = computed(() => sourceControlStore.preferences.branchReadOnly);

View File

@ -2,7 +2,11 @@ import { watch, computed, ref, type ComputedRef } from 'vue';
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
import { Workflow, type IRunExecutionData, type ITaskStartedData } from 'n8n-workflow';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
createWorkflowDocumentId,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import {
copyExecutionData,
@ -108,7 +112,9 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
workflowState.setWorkflowExecutionData(null);
nodeHelpers.updateNodesExecutionIssues();
// Clear partial execution destination to allow full workflow execution
workflowsStore.setChatPartialExecutionDestinationNode(null);
useWorkflowExecutionStateStore(
createWorkflowDocumentId(workflowsStore.workflowId),
).setChatPartialExecutionDestinationNode(null);
void workflowsStore.fetchLastSuccessfulExecution();
}

View File

@ -12,6 +12,7 @@ import { useUIStore } from '@/app/stores/ui.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { waitingNodeTooltip } from '@/features/execution/executions/executions.utils';
import { useExecutionRedaction } from '@/features/execution/executions/composables/useExecutionRedaction';
import uniqBy from 'lodash/uniqBy';
@ -108,6 +109,9 @@ const workflowId = useInjectWorkflowId();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const workflowState = injectWorkflowState();
const router = useRouter();
const { runWorkflow } = useRunWorkflow({ router });
@ -194,7 +198,7 @@ const isMappingEnabled = computed(() => {
return true;
});
const isExecutingPrevious = computed(() => {
if (!workflowsStore.isWorkflowRunning) {
if (!workflowExecutionStateStore.value.isWorkflowRunning) {
return false;
}
const triggeredNode = workflowsStore.executedNode;

View File

@ -28,6 +28,7 @@ import { injectWorkflowState } from '@/app/composables/useWorkflowState';
import { useUIStore } from '@/app/stores/ui.store';
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/app/constants';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
// Types
type RunDataRef = InstanceType<typeof RunData>;
@ -82,6 +83,9 @@ const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowState = injectWorkflowState();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const telemetry = useTelemetry();
const i18n = useI18n();
const activeNode = computed(() => ndvStore.value.activeNode);
@ -162,7 +166,7 @@ const isNodeRunning = computed(() => {
);
});
const workflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const workflowRunning = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
const runTaskData = computed(() => {
if (!node.value || workflowExecution.value === null) {

View File

@ -3,6 +3,7 @@ import { mockedStore, type MockedStore } from '@/__tests__/utils';
import TriggerPanel from './TriggerPanel.vue';
import { createTestingPinia } from '@pinia/testing';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { createTestNode, mockNodeTypeDescription } from '@/__tests__/mocks';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { setActivePinia } from 'pinia';
@ -65,7 +66,9 @@ describe('TriggerPanel.vue', () => {
});
it('renders listening state for webhook node', () => {
workflowsStore.setExecutionWaitingForWebhook(true);
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
true,
);
workflowsStore.executedNode = 'Webhook';
const { getByTestId } = renderComponent(TriggerPanel, {
props: { nodeName: 'Webhook' },
@ -79,7 +82,9 @@ describe('TriggerPanel.vue', () => {
});
it('does not render listening state for other nodes', () => {
workflowsStore.setExecutionWaitingForWebhook(true);
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
true,
);
workflowsStore.executedNode = 'OtherNode';
const { queryByTestId } = renderComponent(TriggerPanel, {
props: { nodeName: 'Webhook' },
@ -93,7 +98,9 @@ describe('TriggerPanel.vue', () => {
});
it('renders listening state when executedNode is a child of the current node', () => {
workflowsStore.setExecutionWaitingForWebhook(true);
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
true,
);
workflowsStore.executedNode = 'ChildNode';
vi.spyOn(workflowDocStore, 'getParentNodes').mockReturnValue(['Webhook']);
const { getByTestId } = renderComponent(TriggerPanel, {
@ -108,7 +115,9 @@ describe('TriggerPanel.vue', () => {
});
it('does not render listening state when executedNode is not a child or current node', () => {
workflowsStore.setExecutionWaitingForWebhook(true);
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
true,
);
workflowsStore.executedNode = 'UnrelatedNode';
const { queryByTestId } = renderComponent(TriggerPanel, {
props: { nodeName: 'Webhook' },

View File

@ -16,6 +16,7 @@ import CopyInput from '@/app/components/CopyInput.vue';
import NodeIcon from '@/app/components/NodeIcon.vue';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { createEventBus } from '@n8n/utils/event-bus';
@ -56,6 +57,9 @@ const nodesTypeStore = useNodeTypesStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const ndvStore = injectNDVStore();
const router = useRouter();
@ -171,7 +175,7 @@ const isListeningForEvents = computed(() => {
return false;
}
if (!workflowsStore.executionWaitingForWebhook) {
if (!workflowExecutionStateStore.value.executionWaitingForWebhook) {
return false;
}
@ -184,7 +188,7 @@ const isListeningForEvents = computed(() => {
return !executedNode || isCurrentNodeExecuted || isChildNodeExecuted;
});
const workflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const workflowRunning = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
const isActivelyPolling = computed(() => {
const triggeredNode = workflowsStore.executedNode;

View File

@ -23,6 +23,7 @@ import type { DataPinningDiscoveryEvent } from '@/app/event-bus';
import { dataPinningEventBus } from '@/app/event-bus';
import { ndvEventBus } from '@/features/ndv/shared/ndv.eventBus';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
@ -70,6 +71,9 @@ const pinnedData = usePinnedData(activeNode);
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const deviceSupport = useDeviceSupport();
const workflowId = useInjectWorkflowId();
const telemetry = useTelemetry();
@ -113,8 +117,8 @@ const showTriggerWaitingWarning = computed(
triggerWaitingWarningEnabled.value &&
!!activeNodeType.value &&
!activeNodeType.value.group.includes('trigger') &&
workflowsStore.isWorkflowRunning &&
workflowsStore.executionWaitingForWebhook,
workflowExecutionStateStore.value.isWorkflowRunning &&
workflowExecutionStateStore.value.executionWaitingForWebhook,
);
const workflowRunData = computed(() => {
@ -314,10 +318,12 @@ const featureRequestUrl = computed(() => {
const outputPanelEditMode = computed(() => ndvStore.value.outputPanelEditMode);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
const isExecutionWaitingForWebhook = computed(
() => workflowExecutionStateStore.value.executionWaitingForWebhook,
);
const blockUi = computed(
() => workflowsStore.isWorkflowRunning || isExecutionWaitingForWebhook.value,
() => workflowExecutionStateStore.value.isWorkflowRunning || isExecutionWaitingForWebhook.value,
);
const foreignCredentials = computed(() =>
@ -430,7 +436,7 @@ const onUnlinkRun = (pane: string) => {
const onNodeExecute = () => {
setTimeout(() => {
if (!activeNode.value || !workflowsStore.isWorkflowRunning) {
if (!activeNode.value || !workflowExecutionStateStore.value.isWorkflowRunning) {
return;
}
triggerWaitingWarningEnabled.value = true;

View File

@ -32,6 +32,7 @@ import { injectNDVStore } from '../ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
import { useI18n } from '@n8n/i18n';
@ -302,7 +303,11 @@ const outputPanelEditMode = computed(() => ndvStore.value.outputPanelEditMode);
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
const isExecutionWaitingForWebhook = computed(
() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId)
.executionWaitingForWebhook,
);
const blockUi = computed(() => isWorkflowRunning.value || isExecutionWaitingForWebhook.value);

View File

@ -5,6 +5,8 @@ import { createTestNode, mockNodeTypeDescription } from '@/__tests__/mocks';
import { mockedStore } from '@/__tests__/utils';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
import { useLogsStore } from '@/app/stores/logs.store';
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
import type { INodeUi } from '@/Interface';
@ -53,6 +55,7 @@ describe('useTriggerExecution', () => {
createTestingPinia();
nodeTypesStore = mockedStore(useNodeTypesStore);
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.workflowId = 'test-workflow-id';
logsStore = mockedStore(useLogsStore);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(null);
@ -127,7 +130,11 @@ describe('useTriggerExecution', () => {
}),
);
logsStore.isOpen = true;
workflowsStore.chatPartialExecutionDestinationNode = 'When chat message received';
vi.spyOn(
useWorkflowExecutionStateStore(createWorkflowDocumentId('test-workflow-id')),
'chatPartialExecutionDestinationNode',
'get',
).mockReturnValue('When chat message received');
const node = ref<INodeUi | null>(chatNode);
const { isInListeningState } = useTriggerExecution(node);
@ -147,7 +154,11 @@ describe('useTriggerExecution', () => {
}),
);
logsStore.isOpen = false;
workflowsStore.chatPartialExecutionDestinationNode = 'When chat message received';
vi.spyOn(
useWorkflowExecutionStateStore(createWorkflowDocumentId('test-workflow-id')),
'chatPartialExecutionDestinationNode',
'get',
).mockReturnValue('When chat message received');
const node = ref<INodeUi | null>(chatNode);
const { isInListeningState } = useTriggerExecution(node);
@ -167,7 +178,11 @@ describe('useTriggerExecution', () => {
}),
);
logsStore.isOpen = true;
workflowsStore.chatPartialExecutionDestinationNode = 'Some Other Node';
vi.spyOn(
useWorkflowExecutionStateStore(createWorkflowDocumentId('test-workflow-id')),
'chatPartialExecutionDestinationNode',
'get',
).mockReturnValue('Some Other Node');
const node = ref<INodeUi | null>(chatNode);
const { isInListeningState } = useTriggerExecution(node);

View File

@ -4,7 +4,7 @@ import { useI18n } from '@n8n/i18n';
import type { INodeUi } from '@/Interface';
import { useNodeExecution, type UseNodeExecutionOptions } from '@/app/composables/useNodeExecution';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { getTriggerNodeServiceName } from '@/app/utils/nodeTypesUtils';
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
import { useLogsStore } from '@/app/stores/logs.store';
@ -21,7 +21,6 @@ export function useTriggerExecution(
) {
const i18n = useI18n();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const logsStore = useLogsStore();
@ -50,7 +49,8 @@ export function useTriggerExecution(
return (
nodeType.value?.name === CHAT_TRIGGER_NODE_TYPE &&
logsStore.isOpen &&
workflowsStore.chatPartialExecutionDestinationNode === nodeValue.value?.name
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId)
.chatPartialExecutionDestinationNode === nodeValue.value?.name
);
});

View File

@ -5,7 +5,7 @@ import { useI18n } from '@n8n/i18n';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants';
import { useLogsStore } from '@/app/stores/logs.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store';
import { computed, useCssModule } from 'vue';
@ -41,8 +41,10 @@ const containerClass = computed(() => ({
const router = useRouter();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const logsStore = useLogsStore();
const chatHubPanelStore = useChatHubPanelStore();
const { runEntireWorkflow } = useRunWorkflow({ router });
@ -62,7 +64,7 @@ const isChatHubOpen = computed(() => chatHubPanelStore.isOpen);
const isChatOpen = computed(() =>
isChatHubAvailable.value ? isChatHubOpen.value : logsStore.isOpen,
);
const isExecuting = computed(() => workflowsStore.isWorkflowRunning);
const isExecuting = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
const testId = computed(() => `execute-workflow-button-${name}`);
function openChat() {
@ -82,7 +84,7 @@ function closeChat() {
}
async function handleClickExecute() {
workflowsStore.setSelectedTriggerNodeName(name);
workflowExecutionStateStore.value.setSelectedTriggerNodeName(name);
await runEntireWorkflow('node', name);
}
</script>

View File

@ -26,6 +26,7 @@ import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import type { IPinData } from 'n8n-workflow';
import {
CanvasConnectionMode,
@ -165,6 +166,14 @@ function setPinData(pinData: IPinData) {
workflowDocumentStore.setPinData(pinData);
}
function setWorkflowRunning(running: boolean) {
const workflowsStore = useWorkflowsStore();
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
vi.spyOn(workflowExecutionStateStore, 'isWorkflowRunning', 'get').mockReturnValue(running);
}
/**
* Populate the render data maps directly for tests
* that rely on per-node inputs/outputs from the render data.
@ -1766,7 +1775,6 @@ describe('useCanvasMapping', () => {
describe('nodeExecutionWaitingForNextById', () => {
it('should be true when already executed node is waiting for next', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const node1 = createTestNode({
name: 'Node 1',
});
@ -1783,7 +1791,7 @@ describe('useCanvasMapping', () => {
workflowState.executingNode.executingNode = [];
workflowState.executingNode.lastAddedExecutingNode = node1.name;
workflowsStore.isWorkflowRunning = true;
setWorkflowRunning(true);
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
nodes: ref(nodes),
@ -1797,7 +1805,6 @@ describe('useCanvasMapping', () => {
});
it('should be false when workflow is not executing', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const node1 = createTestNode({
name: 'Node 1',
});
@ -1814,7 +1821,7 @@ describe('useCanvasMapping', () => {
workflowState.executingNode.executingNode = [];
workflowState.executingNode.lastAddedExecutingNode = node1.name;
workflowsStore.isWorkflowRunning = false;
setWorkflowRunning(false);
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
nodes: ref(nodes),
@ -1828,7 +1835,6 @@ describe('useCanvasMapping', () => {
});
it('should be false when there are nodes that are executing', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const node1 = createTestNode({
name: 'Node 1',
});
@ -1845,7 +1851,7 @@ describe('useCanvasMapping', () => {
workflowState.executingNode.executingNode = [node2.name];
workflowState.executingNode.lastAddedExecutingNode = node1.name;
workflowsStore.isWorkflowRunning = false;
setWorkflowRunning(false);
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
nodes: ref(nodes),
@ -1874,7 +1880,7 @@ describe('useCanvasMapping', () => {
connections,
});
workflowsStore.isWorkflowRunning = true;
setWorkflowRunning(true);
workflowsStore.getWorkflowRunData = {};
setPinData({});
@ -1904,7 +1910,7 @@ describe('useCanvasMapping', () => {
connections,
});
workflowsStore.isWorkflowRunning = true;
setWorkflowRunning(true);
workflowsStore.getWorkflowRunData = {};
setPinData({});
@ -1934,7 +1940,7 @@ describe('useCanvasMapping', () => {
connections,
});
workflowsStore.isWorkflowRunning = true;
setWorkflowRunning(true);
workflowsStore.getWorkflowRunData = {};
setPinData({ 'Manual Trigger': [{ json: {} }] }); // Node has pinned data
@ -1963,7 +1969,7 @@ describe('useCanvasMapping', () => {
connections,
});
workflowsStore.isWorkflowRunning = false;
setWorkflowRunning(false);
workflowsStore.getWorkflowRunData = {};
setPinData({});

View File

@ -7,6 +7,7 @@ import { useI18n } from '@n8n/i18n';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import type { CanvasRenderData } from '../canvas.utils';
import type { Ref } from 'vue';
import { ref, computed } from 'vue';
@ -71,6 +72,9 @@ export function useCanvasMapping({
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionStateStore = computed(() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const workflowState = injectWorkflowState();
const nodeTypesStore = useNodeTypesStore();
const nodeHelpers = useNodeHelpers();
@ -197,7 +201,7 @@ export function useCanvasMapping({
);
const nodeTooltipById = computed(() => {
if (!workflowsStore.isWorkflowRunning) {
if (!workflowExecutionStateStore.value.isWorkflowRunning) {
return {};
}
@ -255,7 +259,7 @@ export function useCanvasMapping({
acc[node.id] =
node.name === workflowState.executingNode.lastAddedExecutingNode &&
workflowState.executingNode.executingNode.length === 0 &&
workflowsStore.isWorkflowRunning;
workflowExecutionStateStore.value.isWorkflowRunning;
return acc;
}, {}),

View File

@ -5,7 +5,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { type IUpdateInformation } from '@/Interface';
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { computed } from 'vue';
@ -22,7 +22,6 @@ const emit = defineEmits<{
dblclickHeader: [MouseEvent];
}>();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const uiStore = useUIStore();
const { renameNode } = useCanvasOperations();
@ -34,7 +33,11 @@ const foreignCredentials = computed(() =>
nodeHelpers.getForeignCredentialsIfSharingEnabled(activeNode.value?.credentials),
);
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
const isExecutionWaitingForWebhook = computed(
() =>
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId)
.executionWaitingForWebhook,
);
const blockUi = computed(() => isWorkflowRunning.value || isExecutionWaitingForWebhook.value);
function handleValueChanged(parameterData: IUpdateInformation) {

View File

@ -44,7 +44,6 @@ vi.mock('@/app/stores/workflowExecutionState.store', () => ({
useWorkflowExecutionStateStore: () => ({
activeExecutionIssuesByNodeName: new Map(),
}),
createWorkflowExecutionStateId: (id: string) => id,
}));
vi.mock('@/app/stores/nodeTypes.store', () => ({

View File

@ -1,3 +1,19 @@
diff --git a/es/components/table/src/table/style-helper.mjs b/es/components/table/src/table/style-helper.mjs
index b07e3e8184b60d293fa9755e00dbcab0a3843e70..be0ebe232c5e15e10cfbdeb5edc29a97eb339c38 100644
--- a/es/components/table/src/table/style-helper.mjs
+++ b/es/components/table/src/table/style-helper.mjs
@@ -70,6 +70,11 @@ function useStyle(props, layout, store, table) {
};
});
const doLayout = () => {
+ // Guard against post-teardown firing: lodash debounce schedules doLayout
+ // via setTimeout, which can fire after vitest tears down jsdom and deletes
+ // requestAnimationFrame from globalThis. The bare reference below would
+ // then throw a ReferenceError that vitest 4 promotes to a test failure.
+ if (typeof requestAnimationFrame === 'undefined') return;
if (shouldUpdateHeight.value) {
layout.updateElsHeight();
}
diff --git a/es/hooks/use-lockscreen/index.mjs b/es/hooks/use-lockscreen/index.mjs
index 482516a6c59f8dcf0caba62b7482f63f126c2280..82a37f344bd650e9d514397b4531c0ff36487c70 100644
--- a/es/hooks/use-lockscreen/index.mjs
@ -11,6 +27,22 @@ index 482516a6c59f8dcf0caba62b7482f63f126c2280..82a37f344bd650e9d514397b4531c0ff
removeClass(document == null ? void 0 : document.body, hiddenCls.value);
if (withoutHiddenClass && document) {
document.body.style.width = bodyWidth;
diff --git a/lib/components/table/src/table/style-helper.js b/lib/components/table/src/table/style-helper.js
index dbf8c403dc7b89f9af550c2ea5047f02bc9f44c6..b80704a3eb614bcfa79da267a5910900f204e002 100644
--- a/lib/components/table/src/table/style-helper.js
+++ b/lib/components/table/src/table/style-helper.js
@@ -74,6 +74,11 @@ function useStyle(props, layout, store, table) {
};
});
const doLayout = () => {
+ // Guard against post-teardown firing: lodash debounce schedules doLayout
+ // via setTimeout, which can fire after vitest tears down jsdom and deletes
+ // requestAnimationFrame from globalThis. The bare reference below would
+ // then throw a ReferenceError that vitest 4 promotes to a test failure.
+ if (typeof requestAnimationFrame === 'undefined') return;
if (shouldUpdateHeight.value) {
layout.updateElsHeight();
}
diff --git a/lib/hooks/use-lockscreen/index.js b/lib/hooks/use-lockscreen/index.js
index ce7bd581a57cd0d7e834c42a954b48d148578ef5..496e4dc07bae546bea037cedb23ea0ee7b3a7955 100644
--- a/lib/hooks/use-lockscreen/index.js

View File

@ -523,7 +523,7 @@ patchedDependencies:
hash: a4b6d56db16fe5870646929938466d6a5c668435fd1551bed6a93fffb597ba42
path: patches/bull@4.16.4.patch
element-plus@2.4.3:
hash: 3bc4ea0a42ad52c6bbc3d06c12c2963d55b57d6b5b8d436e46e7fd8ff8c10661
hash: fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee
path: patches/element-plus@2.4.3.patch
ics:
hash: 163587ad2fa9bc787ed09cd5e958eace08b4aa8aaca651869e9434ba674e158d
@ -3550,7 +3550,7 @@ importers:
version: 2.1.1
element-plus:
specifier: catalog:frontend
version: 2.4.3(patch_hash=3bc4ea0a42ad52c6bbc3d06c12c2963d55b57d6b5b8d436e46e7fd8ff8c10661)(vue@3.5.26(typescript@6.0.2))
version: 2.4.3(patch_hash=fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee)(vue@3.5.26(typescript@6.0.2))
emojibase-data:
specifier: ^17.0.0
version: 17.0.0(emojibase@17.0.0)
@ -4107,7 +4107,7 @@ importers:
version: 3.0.3
element-plus:
specifier: catalog:frontend
version: 2.4.3(patch_hash=3bc4ea0a42ad52c6bbc3d06c12c2963d55b57d6b5b8d436e46e7fd8ff8c10661)(vue@3.5.26(typescript@6.0.2))
version: 2.4.3(patch_hash=fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee)(vue@3.5.26(typescript@6.0.2))
email-providers:
specifier: ^2.0.1
version: 2.0.1
@ -24209,7 +24209,7 @@ snapshots:
'@currents/commit-info': 1.0.1-beta.0
async-retry: 1.3.3
axios: 1.16.1(debug@4.4.3)
axios-retry: 4.5.0(axios@1.16.1(debug@4.4.3))
axios-retry: 4.5.0(axios@1.16.1)
chalk: 4.1.2
commander: 13.1.0
date-fns: 2.30.0
@ -27441,7 +27441,7 @@ snapshots:
'@rudderstack/rudder-sdk-node@3.0.5':
dependencies:
axios: 1.16.1(debug@4.4.3)
axios-retry: 4.5.0(axios@1.16.1(debug@4.4.3))
axios-retry: 4.5.0(axios@1.16.1)
component-type: 2.0.0
join-component: 1.1.0
lodash.clonedeep: 4.5.0
@ -30321,7 +30321,7 @@ snapshots:
axe-core@4.7.2: {}
axios-retry@4.5.0(axios@1.16.1(debug@4.4.3)):
axios-retry@4.5.0(axios@1.16.1):
dependencies:
axios: 1.16.1(debug@4.4.3)
is-retry-allowed: 2.2.0
@ -32164,7 +32164,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
element-plus@2.4.3(patch_hash=3bc4ea0a42ad52c6bbc3d06c12c2963d55b57d6b5b8d436e46e7fd8ff8c10661)(vue@3.5.26(typescript@6.0.2)):
element-plus@2.4.3(patch_hash=fbab57fe3750e430abd5d5e7c04cbf1b6a8f9f1c9676b14c73b77d3e06ba9eee)(vue@3.5.26(typescript@6.0.2)):
dependencies:
'@ctrl/tinycolor': 3.6.0
'@element-plus/icons-vue': 2.3.1(vue@3.5.26(typescript@6.0.2))