mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 18:49:20 +02:00
refactor(editor): Resolve workflow push handlers by id and remove workflow state store and composable (no-changelog) (#31514)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b66d33c305
commit
15749aa39e
|
|
@ -37,7 +37,6 @@ export const STORES = {
|
|||
FOLDERS: 'folders',
|
||||
MODULES: 'modules',
|
||||
FOCUS_PANEL: 'focusPanel',
|
||||
WORKFLOW_STATE: 'workflowState',
|
||||
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
||||
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
|
||||
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
|
||||
|
|
|
|||
|
|
@ -170,11 +170,35 @@ Object.defineProperty(workflowsStore, 'allNodes', {
|
|||
});
|
||||
```
|
||||
|
||||
## Current workflow id (`workflowsStore.workflowId`)
|
||||
|
||||
`workflowsStore.workflowId` is a deprecated global pointer to "the open workflow".
|
||||
It is being removed: consumers become document-store-first and the id is derived
|
||||
from the route. There is **no `storeToRefs` destructuring** of it — every access
|
||||
is the literal `workflowsStore.workflowId` member expression, guarded by ESLint
|
||||
(`no-restricted-syntax`, `warn` during migration, flipped to `error` once empty).
|
||||
|
||||
The per-document store is keyed *by* the id, so it cannot itself answer "which
|
||||
workflow is current" — that comes from the route (or the injected current-document
|
||||
pointer). Pick the replacement by context:
|
||||
|
||||
| Context | Get the current document | Read |
|
||||
|---|---|---|
|
||||
| Components / composables inside `WorkflowLayout` | `injectWorkflowDocumentStore()` (`ShallowRef<Store \| null>`) | `workflowDocumentStore.value?.workflowId ?? ''` |
|
||||
| Out-of-tree Pinia stores / standalone composables | `computed(() => useWorkflowDocumentStore(createWorkflowDocumentId(useWorkflowId().value)))` | `workflowDocumentStore.value.workflowId` |
|
||||
| Non-reactive functions (push handlers) | receive `documentId` via `options`, resolved per event in `usePushConnection.processEvent` from the injected document store | `useWorkflowDocumentStore(documentId)` / `useWorkflowExecutionStateStore(documentId)`, and their `.workflowId` for equality guards |
|
||||
| Lifecycle snapshot (e.g. collaboration) | capture the id into a local/ref at the start of the operation | the captured value |
|
||||
|
||||
Notes:
|
||||
- `useWorkflowId()` (`@/app/composables/useWorkflowId`) resolves `inject(WorkflowIdKey)` first, then the route — use it in any setup context.
|
||||
- Replace `watch(() => workflowsStore.workflowId, …)` with `watch(workflowId, …)` where `workflowId = useWorkflowId()` (or the route-derived computed).
|
||||
- Tests that set `workflowsStore.workflowId = 'x'` (or `workflow.id`) move to providing `WorkflowIdKey` / the route param. Component/composable tests already `provide` `WorkflowIdKey`; push-handler tests pass `documentId` in the handler `options` argument.
|
||||
|
||||
## What NOT to migrate
|
||||
|
||||
- **`workflowsStore.workflowObject`** (39 files) — provides indirect node access via `Workflow` class methods (`.getNode()`, `.nodes`, `.getParentNodes()`, etc.). This is intentionally NOT migrated until both nodes **and** connections move to `workflowDocumentStore`. No ESLint guard for this — it's accepted tech debt.
|
||||
- **Execution-related methods** (e.g., `renameNodeSelectedAndExecution`, `removeNodeExecutionDataById`) — these are not node document state
|
||||
- **`workflowState.executingNode`** and other execution-state properties — these are not node document state
|
||||
- **`workflowExecutionStateStore.executingNode`** and other execution-state properties — these are not node document state (they live in `useWorkflowExecutionStateStore`)
|
||||
|
||||
## Maintaining this recipe
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,17 @@ export default defineConfig(
|
|||
message:
|
||||
'Use workflowDocumentStore.setLastNodeParameters() instead of workflowsStore.setLastNodeParameters()',
|
||||
},
|
||||
{
|
||||
selector: "MemberExpression[property.name='workflowId'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use the workflow document store instead of workflowsStore.workflowId: workflowDocumentStore.workflowId (components/composables via injectWorkflowDocumentStore(); stores via useWorkflowId()) or the documentId from the handler options in push handlers',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.property.name='setWorkflowId'][callee.object.name='workflowsStore']",
|
||||
message:
|
||||
'Do not call workflowsStore.setWorkflowId() — the current workflow id is derived from the route (useWorkflowId())',
|
||||
},
|
||||
],
|
||||
// TODO: Remove these
|
||||
'n8n-local-rules/no-internal-package-import': 'warn',
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
|||
import { computedAsync } from '@vueuse/core';
|
||||
import { useExecutionData } from '@/features/execution/executions/composables/useExecutionData';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import ExperimentalNodeDetailsDrawer from '@/features/workflows/canvas/experimental/components/ExperimentalNodeDetailsDrawer.vue';
|
||||
import { useExperimentalNdvStore } from '@/features/workflows/canvas/experimental/experimentalNdv.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
|
|
@ -51,7 +52,6 @@ import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store';
|
|||
|
||||
import { N8nIcon, N8nInfoTip, N8nInput, N8nRadioButtons, N8nText } from '@n8n/design-system';
|
||||
import { useInjectWorkflowId } from '@/app/composables/useInjectWorkflowId';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
defineOptions({ name: 'FocusPanel' });
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -73,7 +73,9 @@ const nodeHelpers = useNodeHelpers();
|
|||
const focusPanelStore = useFocusPanelStore();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const setupPanelStore = useSetupPanelStore();
|
||||
const telemetry = useTelemetry();
|
||||
|
|
@ -225,7 +227,7 @@ const targetNodeParameterContext = computed<TargetNodeParameterContext | undefin
|
|||
});
|
||||
|
||||
const isNodeExecuting = computed(() =>
|
||||
workflowState.executingNode.isNodeExecuting(node.value?.name ?? ''),
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.value?.name ?? ''),
|
||||
);
|
||||
|
||||
const selectedNodeIds = computed(() => vueFlow.getSelectedNodes.value.map((n) => n.id));
|
||||
|
|
|
|||
|
|
@ -34,11 +34,6 @@ import { usePinnedData } from '@/app/composables/usePinnedData';
|
|||
import { useMessage } from '@/app/composables/useMessage';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import * as buttonParameterUtils from '@/features/ndv/parameters/utils/buttonParameter.utils';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({}),
|
||||
|
|
@ -105,14 +100,6 @@ vi.mock('@/app/composables/useMessage', () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
injectWorkflowDocumentStore: vi.fn(),
|
||||
|
|
@ -128,7 +115,7 @@ let runWorkflow: ReturnType<typeof useRunWorkflow>;
|
|||
let externalHooks: ReturnType<typeof useExternalHooks>;
|
||||
let message: ReturnType<typeof useMessage>;
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
let workflowState: WorkflowState;
|
||||
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
let nodeViewEventBusEmitSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
describe('NodeExecuteButton', () => {
|
||||
|
|
@ -148,8 +135,9 @@ describe('NodeExecuteButton', () => {
|
|||
workflowsStore.workflowId = 'abc123';
|
||||
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('abc123'));
|
||||
vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocumentStore));
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('abc123'),
|
||||
);
|
||||
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId('abc123'));
|
||||
|
|
@ -238,7 +226,7 @@ describe('NodeExecuteButton', () => {
|
|||
it('displays "Stop Listening" when node is running and is a trigger node', () => {
|
||||
const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE });
|
||||
vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node);
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
nodeTypesStore.isTriggerNode = () => true;
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')),
|
||||
|
|
@ -253,7 +241,7 @@ describe('NodeExecuteButton', () => {
|
|||
it('sets button to loading state when node is executing', () => {
|
||||
const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE });
|
||||
vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(node);
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('abc123')),
|
||||
'isWorkflowRunning',
|
||||
|
|
@ -288,7 +276,7 @@ describe('NodeExecuteButton', () => {
|
|||
'isWorkflowRunning',
|
||||
'get',
|
||||
).mockReturnValue(true);
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false);
|
||||
workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(
|
||||
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
|
||||
);
|
||||
|
|
@ -348,8 +336,8 @@ describe('NodeExecuteButton', () => {
|
|||
'get',
|
||||
).mockReturnValue(true);
|
||||
nodeTypesStore.isTriggerNode = () => true;
|
||||
useWorkflowState().setActiveExecutionId('test-execution-id');
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
workflowExecutionStateStore.setActiveExecutionId('test-execution-id');
|
||||
workflowExecutionStateStore.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(
|
||||
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, provide, shallowRef } from 'vue';
|
||||
import {
|
||||
WorkflowDocumentStoreKey,
|
||||
WorkflowIdKey,
|
||||
WorkflowStateKey,
|
||||
} from '@/app/constants/injectionKeys';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { type WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import WorkflowCanvasHostBody from './WorkflowCanvasHostBody.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -34,10 +29,6 @@ const emit = defineEmits<{
|
|||
const localWorkflowId = computed(() => props.workflowId);
|
||||
provide(WorkflowIdKey, localWorkflowId);
|
||||
|
||||
// Scoped workflow state — independent of any sibling editor.
|
||||
const workflowState = useWorkflowState();
|
||||
provide(WorkflowStateKey, workflowState);
|
||||
|
||||
// Document store ref is populated by useWorkflowInitialization in the body.
|
||||
// NodeView and its descendants read it via WorkflowDocumentStoreKey.
|
||||
const currentWorkflowDocumentStore = shallowRef<WorkflowDocumentStore | null>(null);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { computed, onBeforeUnmount, onMounted, watch } from 'vue';
|
||||
import { N8nIcon } from '@n8n/design-system';
|
||||
import { injectStrict } from '@/app/utils/injectStrict';
|
||||
import { WorkflowDocumentStoreKey, WorkflowStateKey } from '@/app/constants/injectionKeys';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { useWorkflowInitialization } from '@/app/composables/useWorkflowInitialization';
|
||||
import MainHeader from '@/app/components/MainHeader/MainHeader.vue';
|
||||
import NodeView from '@/app/views/NodeView.vue';
|
||||
|
|
@ -22,17 +22,15 @@ const emit = defineEmits<{
|
|||
'workflow-loaded': [workflowId: string];
|
||||
}>();
|
||||
|
||||
// Inject the host's scoped provides. Workflow id / state / document store all
|
||||
// resolve to the host's local refs, not the app-level globals.
|
||||
const workflowState = injectStrict(WorkflowStateKey);
|
||||
// Inject the host's scoped provides. Workflow id / document store resolve to the
|
||||
// host's local refs, not the app-level globals.
|
||||
const currentWorkflowDocumentStore = injectStrict(WorkflowDocumentStoreKey);
|
||||
|
||||
const canvasStore = useCanvasStore();
|
||||
const nodeCreatorStore = useNodeCreatorStore();
|
||||
const workflowSaveStore = useWorkflowSaveStore();
|
||||
|
||||
const { isLoading, initializeData, initializeWorkflow, cleanup } =
|
||||
useWorkflowInitialization(workflowState);
|
||||
const { isLoading, initializeData, initializeWorkflow, cleanup } = useWorkflowInitialization();
|
||||
|
||||
// NOTE: push-connection handlers (executionStarted, nodeExecuteAfter, etc.) are
|
||||
// initialized today via MainHeader's onBeforeMount calling
|
||||
|
|
|
|||
|
|
@ -63,11 +63,7 @@ import { useTelemetry } from './useTelemetry';
|
|||
import { useToast } from '@/app/composables/useToast';
|
||||
import * as nodeHelpers from '@/app/composables/useNodeHelpers';
|
||||
import * as workflowsApi from '@/app/api/workflows';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
|
|
@ -158,14 +154,6 @@ vi.mock('@/app/composables/useToast', () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const canPinNodeMock = vi.fn();
|
||||
const setDataMock = vi.fn();
|
||||
const unsetDataMock = vi.fn();
|
||||
|
|
@ -218,7 +206,6 @@ describe('useCanvasOperations', () => {
|
|||
type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||
type WritableDocumentStore = Writable<ReturnType<typeof useWorkflowDocumentStore>>;
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
let workflowDocumentStoreInstance: WritableDocumentStore;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -228,9 +215,6 @@ describe('useCanvasOperations', () => {
|
|||
const pinia = createTestingPinia({ initialState: createInitialState() });
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
|
|
@ -4584,6 +4568,7 @@ describe('useCanvasOperations', () => {
|
|||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const executionsStore = mockedStore(useExecutionsStore);
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
|
||||
const credentialsUpdatedRef = ref(true);
|
||||
const credentialsSpy = vi.spyOn(credentialsUpdatedRef, 'value', 'set');
|
||||
|
|
@ -4594,7 +4579,6 @@ describe('useCanvasOperations', () => {
|
|||
credentialsUpdated: credentialsUpdatedRef,
|
||||
};
|
||||
});
|
||||
const resetStateSpy = vi.spyOn(workflowState, 'resetState');
|
||||
|
||||
nodeCreatorStore.setNodeCreatorState = vi.fn();
|
||||
workflowsStore.removeTestWebhook = vi.fn();
|
||||
|
|
@ -4646,11 +4630,16 @@ describe('useCanvasOperations', () => {
|
|||
createNodeActive: false,
|
||||
});
|
||||
expect(workflowsStore.removeTestWebhook).toHaveBeenCalledWith('workflow-id');
|
||||
expect(resetStateSpy).toHaveBeenCalled();
|
||||
expect(executionStateStore.resetExecutionState).toHaveBeenCalled();
|
||||
expect(builderStore.resetManualExecutionStats).toHaveBeenCalled();
|
||||
expect(resetWorkflowSpy).toHaveBeenCalled();
|
||||
// resetState() must run BEFORE resetWorkflow() — resetState reads workflowId
|
||||
// to target the per-workflow execution-state store, and resetWorkflow empties it.
|
||||
expect(resetStateSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
// The execution-state reset must run BEFORE resetWorkflow() — it targets the
|
||||
// per-workflow execution-state store keyed on workflowId, and resetWorkflow()
|
||||
// empties that id.
|
||||
const resetExecutionStateSpy = executionStateStore.resetExecutionState as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
expect(resetExecutionStateSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
resetWorkflowSpy.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(workflowsStore.currentWorkflowExecutions).toEqual([]);
|
||||
|
|
@ -4776,7 +4765,12 @@ describe('useCanvasOperations', () => {
|
|||
it('should initialize workspace and set execution data when execution is found', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
// Production stages the execution on the store keyed by the document
|
||||
// `initializeWorkspace` opens — i.e. the execution's `workflowData.id`.
|
||||
const setWorkflowExecutionData = vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowId)),
|
||||
'setWorkflowExecutionData',
|
||||
);
|
||||
|
||||
const { openExecution } = useCanvasOperations();
|
||||
|
||||
|
|
@ -6978,8 +6972,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7044,8 +7036,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7108,8 +7098,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7176,8 +7164,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7240,8 +7226,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7320,8 +7304,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7396,8 +7378,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
@ -7474,8 +7454,6 @@ describe('useCanvasOperations', () => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
workflowDocumentStoreInstance = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(useWorkflowsStore().workflowId),
|
||||
) as WritableDocumentStore;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ 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 {
|
||||
disposeWorkflowExecutionStateStore,
|
||||
useWorkflowExecutionStateStore,
|
||||
} from '@/app/stores/workflowExecutionState.store';
|
||||
import type {
|
||||
CanvasConnection,
|
||||
CanvasConnectionCreateData,
|
||||
|
|
@ -127,7 +130,7 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
import { computed, nextTick, ref, type DeepReadonly } from 'vue';
|
||||
import { useUniqueNodeName } from '@/app/composables/useUniqueNodeName';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
|
||||
import { isPresent, tryToParseNumber } from '@/app/utils/typesUtils';
|
||||
import { ensureNodePosition, sanitizeConnections } from '@/app/utils/workflowUtils';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
|
|
@ -187,7 +190,6 @@ type AddNodeOptions = AddNodesBaseOptions & {
|
|||
export function useCanvasOperations() {
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const historyStore = useHistoryStore();
|
||||
const uiStore = useUIStore();
|
||||
|
|
@ -2446,20 +2448,31 @@ export function useCanvasOperations() {
|
|||
createNodeActive: false,
|
||||
});
|
||||
|
||||
// Make sure that if there is a waiting test-webhook, it gets removed
|
||||
const workflowId = workflowsStore.workflowId;
|
||||
const executionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
createWorkflowDocumentId(workflowId),
|
||||
);
|
||||
|
||||
// Make sure that if there is a waiting test-webhook, it gets removed
|
||||
if (executionStateStore.executionWaitingForWebhook) {
|
||||
try {
|
||||
void workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
||||
void workflowsStore.removeTestWebhook(workflowId);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// Reset editable workflow state. resetState() must run BEFORE resetWorkflow()
|
||||
// — it reads ws.workflowId to target the per-workflow execution-state store, and
|
||||
// Reset editable workflow execution state. This must run BEFORE resetWorkflow()
|
||||
// — it targets the per-workflow execution-state store keyed on workflowId, and
|
||||
// resetWorkflow() empties that id.
|
||||
workflowState.resetState();
|
||||
//
|
||||
// Disposes every tracked executionData store + IN_PROGRESS placeholder and clears
|
||||
// all session-level fields, then disposes the per-workflow store so pinia state
|
||||
// doesn't accumulate one entry per workflow opened in this session. Runs
|
||||
// unconditionally so the shared empty-id store (unsaved workflows) is reset too —
|
||||
// otherwise its execution state / executing-node queue would leak across resets.
|
||||
executionStateStore.resetExecutionState();
|
||||
disposeWorkflowExecutionStateStore(executionStateStore);
|
||||
useBuilderStore().resetManualExecutionStats();
|
||||
|
||||
workflowsStore.resetWorkflow();
|
||||
workflowsStore.clearCurrentWorkflowExecutions();
|
||||
workflowsStore.setLastSuccessfulExecution(null);
|
||||
|
|
@ -3216,7 +3229,7 @@ export function useCanvasOperations() {
|
|||
data.workflowData,
|
||||
);
|
||||
|
||||
workflowState.setWorkflowExecutionData(data);
|
||||
useWorkflowExecutionStateStore(openedDocumentStore.documentId).setWorkflowExecutionData(data);
|
||||
|
||||
if (!['manual', 'evaluation'].includes(data.mode)) {
|
||||
// Clear on the store initializeWorkspace just populated — injection
|
||||
|
|
|
|||
|
|
@ -18,21 +18,13 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
import { defineComponent, provide, shallowRef } from 'vue';
|
||||
import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
import { useWorkflowState, injectWorkflowState, type WorkflowState } from './useWorkflowState';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe(useNodeDirtiness, () => {
|
||||
let nodeTypeStore: ReturnType<typeof useNodeTypesStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
|
|
@ -40,7 +32,6 @@ describe(useNodeDirtiness, () => {
|
|||
let historyHelper: ReturnType<typeof useHistoryHelper>;
|
||||
let canvasOperations: ReturnType<typeof useCanvasOperations>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
const NODE_RUN_AT = new Date('2025-01-01T00:00:01');
|
||||
const WORKFLOW_UPDATED_AT = new Date('2025-01-01T00:00:10');
|
||||
|
|
@ -56,8 +47,6 @@ describe(useNodeDirtiness, () => {
|
|||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId(TEST_WORKFLOW_ID);
|
||||
historyHelper = useHistoryHelper({} as RouteLocationNormalizedLoaded);
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(TEST_WORKFLOW_ID),
|
||||
|
|
@ -162,8 +151,9 @@ describe(useNodeDirtiness, () => {
|
|||
|
||||
const runAt = new Date(+WORKFLOW_UPDATED_AT + 1000);
|
||||
|
||||
const workflowState = useWorkflowState();
|
||||
workflowState.setWorkflowExecutionData({
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData({
|
||||
id: workflowsStore.workflowId,
|
||||
finished: true,
|
||||
mode: 'manual',
|
||||
|
|
@ -497,8 +487,9 @@ describe(useNodeDirtiness, () => {
|
|||
workflowDocumentStore.pinNodeData(name, [{ json: {} }]);
|
||||
}
|
||||
|
||||
const workflowState = useWorkflowState();
|
||||
workflowState.setWorkflowExecutionData({
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData({
|
||||
id: workflow.id,
|
||||
finished: true,
|
||||
mode: 'manual',
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ import {
|
|||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { useNodeExecution } from '@/app/composables/useNodeExecution';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { needsAgentInput } from '@/app/utils/nodes/nodeTransforms';
|
||||
import { generateCodeForAiTransform } from '@/features/ndv/parameters/utils/buttonParameter.utils';
|
||||
|
|
@ -49,6 +44,9 @@ const {
|
|||
isWorkflowRunning: false,
|
||||
executionWaitingForWebhook: false,
|
||||
chatPartialExecutionDestinationNode: undefined as string | undefined,
|
||||
executingNode: {
|
||||
isNodeExecuting: vi.fn().mockReturnValue(false),
|
||||
},
|
||||
},
|
||||
mockNodeTypesStore: {
|
||||
getNodeType: vi.fn(),
|
||||
|
|
@ -164,14 +162,6 @@ vi.mock('@/app/composables/useExternalHooks', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/utils/nodes/nodeTransforms', () => ({
|
||||
needsAgentInput: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
|
@ -192,7 +182,6 @@ function createTestNode(overrides: Partial<INodeUi> = {}): INodeUi {
|
|||
} as INodeUi;
|
||||
}
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
describe('useNodeExecution', () => {
|
||||
|
|
@ -200,9 +189,6 @@ describe('useNodeExecution', () => {
|
|||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = vi.mocked(useWorkflowState());
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
uiStore = useUIStore();
|
||||
|
||||
// Reset store properties to defaults
|
||||
|
|
@ -210,6 +196,9 @@ describe('useNodeExecution', () => {
|
|||
mockWorkflowsStore.executedNode = undefined;
|
||||
mockWorkflowExecutionStateStore.executionWaitingForWebhook = false;
|
||||
mockWorkflowExecutionStateStore.chatPartialExecutionDestinationNode = undefined;
|
||||
mockWorkflowExecutionStateStore.executingNode.isNodeExecuting
|
||||
.mockReset()
|
||||
.mockReturnValue(false);
|
||||
mockWorkflowDocumentStore.checkIfNodeHasChatParent.mockReturnValue(false);
|
||||
mockWorkflowsStore.removeTestWebhook.mockReset();
|
||||
mockWorkflowsStore.getNodeByName.mockReset();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import { useMessage } from '@/app/composables/useMessage';
|
|||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useExternalHooks } from '@/app/composables/useExternalHooks';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
import { needsAgentInput } from '@/app/utils/nodes/nodeTransforms';
|
||||
|
|
@ -99,7 +98,6 @@ export function useNodeExecution(
|
|||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
|
|
@ -148,7 +146,7 @@ export function useNodeExecution(
|
|||
return false;
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
return (
|
||||
workflowState.executingNode.isNodeExecuting(nodeRef.value?.name ?? '') ||
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(nodeRef.value?.name ?? '') ||
|
||||
triggeredNode === nodeRef.value?.name
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { jsonParse } from 'n8n-workflow';
|
|||
import { usePostMessageHandler } from './usePostMessageHandler';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
|
||||
const mockImportWorkflowExact = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
|
|
@ -85,6 +86,7 @@ vi.mock('@/features/execution/executions/executions.utils', async (importOrigina
|
|||
const mockRoute = vi.hoisted(() => ({
|
||||
name: 'workflow' as string,
|
||||
query: {} as Record<string, string>,
|
||||
params: {} as Record<string, string>,
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as object;
|
||||
|
|
@ -94,12 +96,6 @@ vi.mock('vue-router', async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
function createMockWorkflowState(): WorkflowState {
|
||||
return {
|
||||
setWorkflowExecutionData: vi.fn(),
|
||||
} as unknown as WorkflowState;
|
||||
}
|
||||
|
||||
function dispatchPostMessage(payload: Record<string, unknown>) {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
|
|
@ -109,17 +105,23 @@ function dispatchPostMessage(payload: Record<string, unknown>) {
|
|||
}
|
||||
|
||||
describe('usePostMessageHandler', () => {
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setActivePinia(createTestingPinia());
|
||||
mockIsProductionExecutionPreview.value = false;
|
||||
mockRoute.name = 'workflow';
|
||||
mockRoute.query = {};
|
||||
workflowState = createMockWorkflowState();
|
||||
mockRoute.params = {};
|
||||
});
|
||||
|
||||
// The handler resolves the execution-state store via
|
||||
// `currentWorkflowDocumentStore.value.documentId`. Tests pass a mock document
|
||||
// store whose `documentId` is `createWorkflowDocumentId('')`, so this returns the
|
||||
// same store instance the handler writes to.
|
||||
function getExecutionStateStore() {
|
||||
return useWorkflowExecutionStateStore(createWorkflowDocumentId(''));
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
// Ensure listeners are cleaned up
|
||||
});
|
||||
|
|
@ -128,7 +130,6 @@ describe('usePostMessageHandler', () => {
|
|||
it('should add message event listener on setup', () => {
|
||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||
const { setup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +141,6 @@ describe('usePostMessageHandler', () => {
|
|||
it('should remove message event listener on cleanup', () => {
|
||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +153,6 @@ describe('usePostMessageHandler', () => {
|
|||
it('should emit n8nReady postMessage on setup', () => {
|
||||
const postMessageSpy = vi.spyOn(window.parent, 'postMessage');
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
|
||||
|
|
@ -170,7 +169,6 @@ describe('usePostMessageHandler', () => {
|
|||
it('should include pushRef in n8nReady postMessage', () => {
|
||||
const postMessageSpy = vi.spyOn(window.parent, 'postMessage');
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
|
||||
|
|
@ -190,7 +188,6 @@ describe('usePostMessageHandler', () => {
|
|||
describe('openWorkflow command', () => {
|
||||
it('should call importWorkflowExact when openWorkflow message is received', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -213,7 +210,6 @@ describe('usePostMessageHandler', () => {
|
|||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
const uiStore = useUIStore();
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -244,7 +240,6 @@ describe('usePostMessageHandler', () => {
|
|||
const uiStore = useUIStore();
|
||||
uiStore.setNotificationsSuppressed(true, { allowErrors: true });
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -275,7 +270,6 @@ describe('usePostMessageHandler', () => {
|
|||
const uiStore = useUIStore();
|
||||
uiStore.setNotificationsSuppressed(true, { allowErrors: true });
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -303,7 +297,6 @@ describe('usePostMessageHandler', () => {
|
|||
mockRoute.name = 'WorkflowDemo';
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -328,7 +321,6 @@ describe('usePostMessageHandler', () => {
|
|||
mockRoute.query = { canExecute: 'true' };
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -350,7 +342,6 @@ describe('usePostMessageHandler', () => {
|
|||
|
||||
it('should emit tidyUp event when tidyUp is true', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -383,7 +374,6 @@ describe('usePostMessageHandler', () => {
|
|||
});
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -419,7 +409,6 @@ describe('usePostMessageHandler', () => {
|
|||
|
||||
const storeRef = shallowRef(null);
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: storeRef,
|
||||
});
|
||||
setup();
|
||||
|
|
@ -450,7 +439,6 @@ describe('usePostMessageHandler', () => {
|
|||
});
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -479,7 +467,6 @@ describe('usePostMessageHandler', () => {
|
|||
mockOpenExecution.mockResolvedValue(null);
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -506,7 +493,6 @@ describe('usePostMessageHandler', () => {
|
|||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
const uiStore = useUIStore();
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -551,7 +537,6 @@ describe('usePostMessageHandler', () => {
|
|||
});
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -578,14 +563,19 @@ describe('usePostMessageHandler', () => {
|
|||
describe('openExecutionPreview command', () => {
|
||||
it('should call importWorkflowExact and set execution data', async () => {
|
||||
const mockSetPinData = vi.fn();
|
||||
const storeRef = shallowRef({ setPinData: mockSetPinData } as never);
|
||||
const storeRef = shallowRef({
|
||||
documentId: createWorkflowDocumentId(''),
|
||||
setPinData: mockSetPinData,
|
||||
} as never);
|
||||
const mockExecutionData = {
|
||||
workflowData: { id: 'w1' },
|
||||
} as unknown as IExecutionResponse;
|
||||
mockBuildExecutionResponseFromSchema.mockReturnValue(mockExecutionData);
|
||||
|
||||
const executionStateStore = getExecutionStateStore();
|
||||
const setWorkflowExecutionDataSpy = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: storeRef,
|
||||
});
|
||||
setup();
|
||||
|
|
@ -604,7 +594,7 @@ describe('usePostMessageHandler', () => {
|
|||
expect(mockImportWorkflowExact).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(workflowState.setWorkflowExecutionData).toHaveBeenCalledWith(mockExecutionData);
|
||||
expect(setWorkflowExecutionDataSpy).toHaveBeenCalledWith(mockExecutionData);
|
||||
expect(mockSetPinData).toHaveBeenCalledWith({});
|
||||
|
||||
cleanup();
|
||||
|
|
@ -622,10 +612,12 @@ describe('usePostMessageHandler', () => {
|
|||
});
|
||||
|
||||
const mockSetPinData = vi.fn();
|
||||
const storeRef = shallowRef({ setPinData: mockSetPinData } as never);
|
||||
const storeRef = shallowRef({
|
||||
documentId: createWorkflowDocumentId(''),
|
||||
setPinData: mockSetPinData,
|
||||
} as never);
|
||||
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: storeRef,
|
||||
});
|
||||
setup();
|
||||
|
|
@ -660,7 +652,6 @@ describe('usePostMessageHandler', () => {
|
|||
|
||||
it('should throw if workflow has no nodes', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -687,7 +678,6 @@ describe('usePostMessageHandler', () => {
|
|||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
const uiStore = useUIStore();
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -728,7 +718,6 @@ describe('usePostMessageHandler', () => {
|
|||
describe('fitView command', () => {
|
||||
it('should emit fitView on canvasEventBus when fitView message is received', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -746,7 +735,6 @@ describe('usePostMessageHandler', () => {
|
|||
describe('message filtering', () => {
|
||||
it('should ignore non-string messages', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
@ -763,7 +751,6 @@ describe('usePostMessageHandler', () => {
|
|||
|
||||
it('should ignore messages without "command" in data', async () => {
|
||||
const { setup, cleanup } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore: shallowRef(null),
|
||||
});
|
||||
setup();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { buildExecutionResponseFromSchema } from '@/features/execution/execution
|
|||
import type { ExecutionPreviewNodeSchema } from '@/features/execution/executions/executions.types';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import {
|
||||
useWorkflowDocumentStore as createWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
|
|
@ -27,16 +26,13 @@ import {
|
|||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowImport } from '@/app/composables/useWorkflowImport';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
|
||||
interface PostMessageHandlerDeps {
|
||||
workflowState: WorkflowState;
|
||||
currentWorkflowDocumentStore: ShallowRef<WorkflowDocumentStore | null>;
|
||||
}
|
||||
|
||||
export function usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore,
|
||||
}: PostMessageHandlerDeps) {
|
||||
export function usePostMessageHandler({ currentWorkflowDocumentStore }: PostMessageHandlerDeps) {
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const canvasStore = useCanvasStore();
|
||||
|
|
@ -105,7 +101,10 @@ export function usePostMessageHandler({
|
|||
// "execution starting"). The user-triggered execution flow will handle
|
||||
// activeExecutionId itself.
|
||||
if (window !== window.parent && route.query.canExecute !== 'true') {
|
||||
workflowState.setActiveExecutionId(null);
|
||||
const workflowDocumentStore = currentWorkflowDocumentStore.value;
|
||||
if (workflowDocumentStore) {
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.documentId).setActiveExecutionId(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.tidyUp === true) {
|
||||
|
|
@ -201,8 +200,13 @@ export function usePostMessageHandler({
|
|||
|
||||
await importWorkflowExact(json);
|
||||
|
||||
workflowState.setWorkflowExecutionData(data);
|
||||
currentWorkflowDocumentStore.value?.setPinData({});
|
||||
const workflowDocumentStore = currentWorkflowDocumentStore.value;
|
||||
if (workflowDocumentStore) {
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.documentId).setWorkflowExecutionData(
|
||||
data,
|
||||
);
|
||||
workflowDocumentStore.setPinData({});
|
||||
}
|
||||
|
||||
canvasStore.stopLoading();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { EVALUATION_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
|||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import type { Router } from 'vue-router';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
|
|
@ -28,10 +27,13 @@ import { mockedStore } from '@/__tests__/utils';
|
|||
import { useReadyToRunStore } from '@/features/workflows/readyToRun/stores/readyToRun.store';
|
||||
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
const opts = {
|
||||
workflowState: mock<WorkflowState>(),
|
||||
const documentId = createWorkflowDocumentId('1');
|
||||
const opts: PushHandlerOptions = {
|
||||
router: mock<Router>(),
|
||||
documentId,
|
||||
};
|
||||
|
||||
const mockShowMessage = vi.fn();
|
||||
|
|
@ -61,6 +63,8 @@ describe('continueEvaluationLoop()', () => {
|
|||
});
|
||||
|
||||
it('should call runWorkflow() if workflow has eval trigger that executed successfully with rows left', () => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const evalTriggerNodeName = 'eval-trigger';
|
||||
const evalTriggerNodeData = mock<ITaskData>({
|
||||
data: {
|
||||
|
|
@ -103,6 +107,10 @@ describe('continueEvaluationLoop()', () => {
|
|||
nodeData: evalTriggerNodeData,
|
||||
rerunTriggerNode: true,
|
||||
});
|
||||
// The rerun must be bound to the document whose execution just finished (not the
|
||||
// globally-current workflow) so it serializes and saves the correct workflow.
|
||||
const runWorkflowOptions = vi.mocked(useRunWorkflow).mock.calls.at(-1)?.[0];
|
||||
expect(runWorkflowOptions?.workflowDocumentStore?.value.documentId).toBe(documentId);
|
||||
});
|
||||
|
||||
it('should not call runWorkflow() if workflow execution status is not success', () => {
|
||||
|
|
@ -250,11 +258,9 @@ describe('executionFinished', () => {
|
|||
});
|
||||
|
||||
it('should clear lastAddedExecutingNode when execution is finished', async () => {
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: 'test-node',
|
||||
},
|
||||
});
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
workflowExecutionStateStore.executingNode.lastAddedExecutingNode = 'test-node';
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -264,27 +270,21 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(workflowState.executingNode.lastAddedExecutingNode).toBeNull();
|
||||
expect(workflowExecutionStateStore.executingNode.lastAddedExecutingNode).toBeNull();
|
||||
});
|
||||
|
||||
describe('ready-to-run AI workflow tracking', () => {
|
||||
it('should track successful execution of ready-to-run-ai-workflow', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
useWorkflowExecutionStateStore(documentId),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
|
|
@ -299,12 +299,6 @@ describe('executionFinished', () => {
|
|||
'trackExecuteAiWorkflowSuccess',
|
||||
);
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: null,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -314,26 +308,20 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(trackExecuteAiWorkflowSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should track failed execution of ready-to-run-ai-workflow', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
useWorkflowExecutionStateStore(documentId),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
|
|
@ -345,12 +333,6 @@ describe('executionFinished', () => {
|
|||
|
||||
const trackExecuteAiWorkflow = vi.spyOn(readyToRunStore, 'trackExecuteAiWorkflow');
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: null,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -360,26 +342,20 @@ describe('executionFinished', () => {
|
|||
status: 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(trackExecuteAiWorkflow).toHaveBeenCalledWith('error');
|
||||
});
|
||||
|
||||
it('should track execution of ready-to-run-ai-workflow-v5', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
useWorkflowExecutionStateStore(documentId),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
|
|
@ -394,12 +370,6 @@ describe('executionFinished', () => {
|
|||
'trackExecuteAiWorkflowSuccess',
|
||||
);
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: null,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -409,26 +379,20 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(trackExecuteAiWorkflowSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should track execution of ready-to-run-ai-workflow-v6', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
useWorkflowExecutionStateStore(documentId),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
|
|
@ -440,12 +404,6 @@ describe('executionFinished', () => {
|
|||
|
||||
const trackExecuteAiWorkflow = vi.spyOn(readyToRunStore, 'trackExecuteAiWorkflow');
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: null,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -455,26 +413,20 @@ describe('executionFinished', () => {
|
|||
status: 'canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(trackExecuteAiWorkflow).toHaveBeenCalledWith('canceled');
|
||||
});
|
||||
|
||||
it('should not track execution for non-ready-to-run workflows', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
useWorkflowExecutionStateStore(documentId),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
|
|
@ -490,12 +442,6 @@ describe('executionFinished', () => {
|
|||
);
|
||||
const trackExecuteAiWorkflow = vi.spyOn(readyToRunStore, 'trackExecuteAiWorkflow');
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: null,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -505,10 +451,7 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(trackExecuteAiWorkflowSuccess).not.toHaveBeenCalled();
|
||||
|
|
@ -516,30 +459,15 @@ describe('executionFinished', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should return early and clear active execution when fetchExecutionData returns undefined', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
workflows: {
|
||||
activeExecutionId: '123',
|
||||
},
|
||||
},
|
||||
});
|
||||
it('should set active execution id to undefined when execution data is not stored', async () => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const workflowsListStore = mockedStore(useWorkflowsListStore);
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
// Set workflowId + activeExecutionId via the state store
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
vi.spyOn(workflowExecutionStateStore, 'activeExecutionId', 'get').mockReturnValue('123');
|
||||
|
||||
// Mock getWorkflowById to return a workflow
|
||||
vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
|
|
@ -549,17 +477,10 @@ describe('executionFinished', () => {
|
|||
settings: {},
|
||||
} as unknown as ReturnType<typeof workflowsListStore.getWorkflowById>);
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue(null);
|
||||
vi.spyOn(useWorkflowsStore(), 'fetchExecutionDataById').mockResolvedValue(null);
|
||||
|
||||
const setProcessingExecutionResultsSpy = vi.spyOn(uiStore, 'setProcessingExecutionResults');
|
||||
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: 'test-node',
|
||||
},
|
||||
setActiveExecutionId: vi.fn(),
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -569,14 +490,11 @@ describe('executionFinished', () => {
|
|||
status: 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
// Verify that setActiveExecutionId was called with undefined
|
||||
expect(workflowState.setActiveExecutionId).toHaveBeenCalledWith(undefined);
|
||||
expect(workflowExecutionStateStore.setActiveExecutionId).toHaveBeenCalledWith(undefined);
|
||||
|
||||
// Verify that processing was set to false
|
||||
expect(setProcessingExecutionResultsSpy).toHaveBeenCalledWith(false);
|
||||
|
|
@ -585,25 +503,16 @@ describe('executionFinished', () => {
|
|||
});
|
||||
|
||||
it('should clear executing node queue even when fetchExecutionData returns undefined', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
workflows: {
|
||||
activeExecutionId: '123',
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const workflowsListStore = mockedStore(useWorkflowsListStore);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const clearNodeExecutionQueue = vi.spyOn(
|
||||
workflowExecutionStateStore.executingNode,
|
||||
'clearNodeExecutionQueue',
|
||||
);
|
||||
|
||||
workflowsStore.workflowId = '1';
|
||||
vi.spyOn(
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')),
|
||||
'activeExecutionId',
|
||||
'get',
|
||||
).mockReturnValue('123');
|
||||
vi.spyOn(workflowExecutionStateStore, 'activeExecutionId', 'get').mockReturnValue('123');
|
||||
|
||||
vi.spyOn(workflowsListStore, 'getWorkflowById').mockReturnValue({
|
||||
id: '1',
|
||||
|
|
@ -615,16 +524,7 @@ describe('executionFinished', () => {
|
|||
} as unknown as ReturnType<typeof workflowsListStore.getWorkflowById>);
|
||||
|
||||
// Simulate the iframe scenario: fetch returns no data
|
||||
vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue(null);
|
||||
|
||||
const clearNodeExecutionQueue = vi.fn();
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: 'LastNode',
|
||||
clearNodeExecutionQueue,
|
||||
},
|
||||
setActiveExecutionId: vi.fn(),
|
||||
});
|
||||
vi.spyOn(useWorkflowsStore(), 'fetchExecutionDataById').mockResolvedValue(null);
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
|
|
@ -635,10 +535,7 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
// The executing node queue must be cleared so nodes don't stay stuck
|
||||
|
|
@ -647,21 +544,15 @@ describe('executionFinished', () => {
|
|||
});
|
||||
|
||||
it('should clear executing node queue when activeExecutionId is undefined (iframe preview)', async () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsStore.workflowId = '1';
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const clearNodeExecutionQueue = vi.spyOn(
|
||||
workflowExecutionStateStore.executingNode,
|
||||
'clearNodeExecutionQueue',
|
||||
);
|
||||
// In iframe preview after resetWorkspace, activeExecutionId is undefined by default.
|
||||
|
||||
const clearNodeExecutionQueue = vi.fn();
|
||||
const workflowState = mock<WorkflowState>({
|
||||
executingNode: {
|
||||
lastAddedExecutingNode: 'LastNode',
|
||||
clearNodeExecutionQueue,
|
||||
},
|
||||
});
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
|
|
@ -671,16 +562,65 @@ describe('executionFinished', () => {
|
|||
status: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
router: mock<Router>(),
|
||||
workflowState,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
// Even when activeExecutionId is undefined (iframe early return),
|
||||
// the executing node queue must be cleared.
|
||||
expect(clearNodeExecutionQueue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores a finish for an execution this document is not tracking', async () => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
// This document is tracking a different execution (e.g. a concurrent
|
||||
// scheduled run of the same workflow finished). Even though the workflow id
|
||||
// matches, the finish must not clear this document's active execution nor its
|
||||
// per-node spinner queue.
|
||||
vi.spyOn(workflowExecutionStateStore, 'activeExecutionId', 'get').mockReturnValue('our-exec');
|
||||
workflowExecutionStateStore.executingNode.lastAddedExecutingNode = 'busy-node';
|
||||
const clearNodeExecutionQueue = vi.spyOn(
|
||||
workflowExecutionStateStore.executingNode,
|
||||
'clearNodeExecutionQueue',
|
||||
);
|
||||
const fetchSpy = vi.spyOn(workflowsStore, 'fetchExecutionDataById');
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
data: { executionId: 'foreign-exec', workflowId: '1', status: 'success' },
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
expect(workflowExecutionStateStore.setActiveExecutionId).not.toHaveBeenCalled();
|
||||
expect(clearNodeExecutionQueue).not.toHaveBeenCalled();
|
||||
expect(workflowExecutionStateStore.executingNode.lastAddedExecutingNode).toBe('busy-node');
|
||||
});
|
||||
|
||||
it('processes a pending finish (null active) when the workflow id matches', async () => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
// The finish raced ahead of executionStarted: active is still pending (null).
|
||||
// Fall back to the workflow id so our own run's finish isn't dropped.
|
||||
vi.spyOn(workflowExecutionStateStore, 'activeExecutionId', 'get').mockReturnValue(null);
|
||||
const fetchSpy = vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue(null);
|
||||
|
||||
await executionFinished(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
data: { executionId: 'exec-x', workflowId: '1', status: 'error' },
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('exec-x');
|
||||
});
|
||||
});
|
||||
|
||||
describe('manual execution stats tracking', () => {
|
||||
|
|
@ -690,37 +630,34 @@ describe('manual execution stats tracking', () => {
|
|||
|
||||
describe('handleExecutionFinishedWithSuccessOrOther', () => {
|
||||
it('increments success stats on successful execution', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
const incrementSpy = vi.spyOn(builderStore, 'incrementManualExecutionStats');
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'success', false);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'success', false);
|
||||
|
||||
expect(incrementSpy).toHaveBeenCalledWith('success');
|
||||
});
|
||||
|
||||
it('does not increment success stats when successToastAlreadyShown is true', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
const incrementSpy = vi.spyOn(builderStore, 'incrementManualExecutionStats');
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'success', true);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'success', true);
|
||||
|
||||
expect(incrementSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not increment stats for non-success status', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
const incrementSpy = vi.spyOn(builderStore, 'incrementManualExecutionStats');
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'error', false);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'error', false);
|
||||
|
||||
expect(incrementSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -732,8 +669,7 @@ describe('manual execution stats tracking', () => {
|
|||
});
|
||||
|
||||
it('shows success toast when executed node has run data', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
|
@ -758,14 +694,13 @@ describe('manual execution stats tracking', () => {
|
|||
nodeTypesStore.getNodeType = () =>
|
||||
mock<INodeTypeDescription>({ polling: undefined, group: [] });
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'success', false);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'success', false);
|
||||
|
||||
expect(mockShowMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
|
||||
});
|
||||
|
||||
it('shows warning toast when executed node was not reached', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
|
@ -788,14 +723,13 @@ describe('manual execution stats tracking', () => {
|
|||
nodeTypesStore.getNodeType = () =>
|
||||
mock<INodeTypeDescription>({ polling: undefined, group: [] });
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'success', false);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'success', false);
|
||||
|
||||
expect(mockShowMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'warning' }));
|
||||
});
|
||||
|
||||
it('does not show warning toast when successToastAlreadyShown is true', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
|
@ -818,7 +752,7 @@ describe('manual execution stats tracking', () => {
|
|||
nodeTypesStore.getNodeType = () =>
|
||||
mock<INodeTypeDescription>({ polling: undefined, group: [] });
|
||||
|
||||
handleExecutionFinishedWithSuccessOrOther(mock<WorkflowState>(), 'success', true);
|
||||
handleExecutionFinishedWithSuccessOrOther(createWorkflowDocumentId(''), 'success', true);
|
||||
|
||||
expect(mockShowMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -826,8 +760,7 @@ describe('manual execution stats tracking', () => {
|
|||
|
||||
describe('handleExecutionFinishedWithErrorOrCanceled', () => {
|
||||
it('increments error stats on execution error', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
const incrementSpy = vi.spyOn(builderStore, 'incrementManualExecutionStats');
|
||||
|
|
@ -844,14 +777,14 @@ describe('manual execution stats tracking', () => {
|
|||
handleExecutionFinishedWithErrorOrCanceled(
|
||||
execution,
|
||||
mock<IRunExecutionData>({ resultData: { error: { message: 'test', name: 'Error' } } }),
|
||||
createWorkflowDocumentId(''),
|
||||
);
|
||||
|
||||
expect(incrementSpy).toHaveBeenCalledWith('error');
|
||||
});
|
||||
|
||||
it('does not increment stats for canceled executions', () => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
const builderStore = mockedStore(useBuilderStore);
|
||||
const incrementSpy = vi.spyOn(builderStore, 'incrementManualExecutionStats');
|
||||
|
|
@ -863,6 +796,7 @@ describe('manual execution stats tracking', () => {
|
|||
handleExecutionFinishedWithErrorOrCanceled(
|
||||
execution,
|
||||
mock<IRunExecutionData>({ resultData: {} }),
|
||||
createWorkflowDocumentId(''),
|
||||
);
|
||||
|
||||
expect(incrementSpy).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { computed } from 'vue';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import { useExternalHooks } from '@/app/composables/useExternalHooks';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
|
|
@ -17,6 +18,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
|||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
|
|
@ -46,42 +48,48 @@ import {
|
|||
TelemetryHelpers,
|
||||
createRunExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { type WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export type SimplifiedExecution = Pick<
|
||||
IExecutionResponse,
|
||||
'workflowId' | 'workflowData' | 'data' | 'status' | 'startedAt' | 'stoppedAt' | 'id'
|
||||
>;
|
||||
|
||||
export type ExecutionFinishedOptions = {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
workflowState: WorkflowState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the 'executionFinished' event, which happens when a workflow execution is finished.
|
||||
*/
|
||||
export async function executionFinished(
|
||||
{ data }: ExecutionFinished,
|
||||
options: ExecutionFinishedOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
export async function executionFinished({ data }: ExecutionFinished, options: PushHandlerOptions) {
|
||||
const { documentId } = options;
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const uiStore = useUIStore();
|
||||
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
|
||||
options.workflowState.executingNode.lastAddedExecutingNode = null;
|
||||
options.workflowState.executingNode.clearNodeExecutionQueue();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
// Only act on the finish of the execution this document is actually tracking.
|
||||
// Normal match is on the execution id; when the active execution is still
|
||||
// pending (null) because this finish raced ahead of `executionStarted`, fall
|
||||
// back to the document's workflow id so we don't drop our own run's finish.
|
||||
// This rejects finishes from other workflows (which would otherwise clear this
|
||||
// document's running state and show a spurious toast) and from concurrent runs
|
||||
// of the same workflow that this document isn't displaying.
|
||||
const { activeExecutionId } = workflowExecutionStateStore;
|
||||
const belongsToThisDocument =
|
||||
activeExecutionId === data.executionId ||
|
||||
(activeExecutionId === null && data.workflowId === workflowExecutionStateStore.workflowId);
|
||||
|
||||
// No workflow is actively running, therefore we ignore this event
|
||||
if (typeof workflowExecutionStateStore.activeExecutionId === 'undefined') {
|
||||
// Clear the per-node spinner queue when this finish is ours, or when this
|
||||
// document isn't tracking any run (`undefined`, e.g. idle or iframe preview)
|
||||
// so stale spinners don't get stuck. Skip clearing only while a different live
|
||||
// execution is being tracked, so a foreign finish can't wipe this document's
|
||||
// running state. `clearNodeExecutionQueue` also resets `lastAddedExecutingNode`.
|
||||
if (belongsToThisDocument || activeExecutionId === undefined) {
|
||||
workflowExecutionStateStore.executingNode.clearNodeExecutionQueue();
|
||||
}
|
||||
|
||||
if (!belongsToThisDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,15 +134,11 @@ export async function executionFinished(
|
|||
let successToastAlreadyShown = false;
|
||||
|
||||
if (data.status === 'success') {
|
||||
handleExecutionFinishedWithSuccessOrOther(
|
||||
options.workflowState,
|
||||
data.status,
|
||||
successToastAlreadyShown,
|
||||
);
|
||||
handleExecutionFinishedWithSuccessOrOther(documentId, data.status, successToastAlreadyShown);
|
||||
successToastAlreadyShown = true;
|
||||
}
|
||||
|
||||
const execution = await fetchExecutionData(data.executionId);
|
||||
const execution = await fetchExecutionData(data.executionId, documentId);
|
||||
|
||||
/**
|
||||
* This accounts for the case where the execution is not stored.
|
||||
|
|
@ -142,7 +146,7 @@ export async function executionFinished(
|
|||
* Returning early presists existing run data up to this point.
|
||||
*/
|
||||
if (!execution) {
|
||||
options.workflowState.setActiveExecutionId(undefined);
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
uiStore.setProcessingExecutionResults(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -153,16 +157,16 @@ export async function executionFinished(
|
|||
if (execution.data?.waitTill !== undefined) {
|
||||
handleExecutionFinishedWithWaitTill(data.workflowId, options);
|
||||
} else if (execution.status === 'error' || execution.status === 'canceled') {
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, documentId);
|
||||
} else {
|
||||
handleExecutionFinishedWithSuccessOrOther(
|
||||
options.workflowState,
|
||||
documentId,
|
||||
execution.status,
|
||||
successToastAlreadyShown,
|
||||
);
|
||||
}
|
||||
|
||||
setRunExecutionData(execution, runExecutionData, options.workflowState);
|
||||
setRunExecutionData(execution, runExecutionData, documentId);
|
||||
|
||||
continueEvaluationLoop(execution, options);
|
||||
}
|
||||
|
|
@ -170,12 +174,9 @@ export async function executionFinished(
|
|||
/**
|
||||
* Implicit looping: This will re-trigger the evaluation trigger if it exists on a successful execution of the workflow.
|
||||
* @param execution
|
||||
* @param router
|
||||
* @param opts
|
||||
*/
|
||||
export function continueEvaluationLoop(
|
||||
execution: SimplifiedExecution,
|
||||
opts: ExecutionFinishedOptions,
|
||||
) {
|
||||
export function continueEvaluationLoop(execution: SimplifiedExecution, opts: PushHandlerOptions) {
|
||||
if (execution.status !== 'success' || execution.data?.startData?.destinationNode !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -196,7 +197,14 @@ export function continueEvaluationLoop(
|
|||
const rowsLeft = mainData ? (mainData[0]?.json?._rowsLeft as number) : 0;
|
||||
|
||||
if (rowsLeft && rowsLeft > 0) {
|
||||
const { runWorkflow } = useRunWorkflow(opts);
|
||||
// useRunWorkflow needs a workflow document store; inject() doesn't resolve in
|
||||
// this async, non-setup context, so bind it explicitly to the document whose
|
||||
// execution just finished — otherwise the rerun targets the globally-current
|
||||
// workflow instead of this one.
|
||||
const { runWorkflow } = useRunWorkflow({
|
||||
router: opts.router,
|
||||
workflowDocumentStore: computed(() => useWorkflowDocumentStore(opts.documentId)),
|
||||
});
|
||||
void runWorkflow({
|
||||
triggerNode: evaluationTrigger.name,
|
||||
// pass output of previous node run to trigger next run
|
||||
|
|
@ -211,11 +219,10 @@ export function continueEvaluationLoop(
|
|||
*/
|
||||
export async function fetchExecutionData(
|
||||
executionId: string,
|
||||
documentId: WorkflowDocumentId,
|
||||
): Promise<SimplifiedExecution | undefined> {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
|
||||
try {
|
||||
const executionResponse = await workflowsStore.fetchExecutionDataById(executionId);
|
||||
|
|
@ -253,16 +260,16 @@ export function getRunExecutionData(execution: SimplifiedExecution): IRunExecuti
|
|||
* Returns the error message for the execution run data if the execution status is crashed or canceled,
|
||||
* or a fallback error message otherwise
|
||||
*/
|
||||
export function getRunDataExecutedErrorMessage(execution: SimplifiedExecution) {
|
||||
export function getRunDataExecutedErrorMessage(
|
||||
execution: SimplifiedExecution,
|
||||
documentId: WorkflowDocumentId,
|
||||
) {
|
||||
const i18n = useI18n();
|
||||
|
||||
if (execution.status === 'crashed') {
|
||||
return i18n.baseText('pushConnection.executionFailed.message');
|
||||
} else if (execution.status === 'canceled') {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
return i18n.baseText('executionsList.showMessage.stopExecution.message', {
|
||||
interpolate: { activeExecutionId: workflowExecutionStateStore.activeExecutionId ?? '' },
|
||||
|
|
@ -281,13 +288,11 @@ export function getRunDataExecutedErrorMessage(execution: SimplifiedExecution) {
|
|||
*/
|
||||
export function handleExecutionFinishedWithWaitTill(
|
||||
workflowId: string,
|
||||
options: {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
},
|
||||
options: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const workflowSaving = useWorkflowSaving(options);
|
||||
const workflowSaving = useWorkflowSaving({ router: options.router });
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
const workflowSettings = workflowDocumentStore.settings;
|
||||
|
|
@ -300,7 +305,9 @@ export function handleExecutionFinishedWithWaitTill(
|
|||
globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
|
||||
key: 'open-settings',
|
||||
action: async () => {
|
||||
if (!workflowsStore.isWorkflowSaved[workflowsStore.workflowId])
|
||||
if (
|
||||
!workflowsStore.isWorkflowSaved[useWorkflowDocumentStore(options.documentId).workflowId]
|
||||
)
|
||||
await workflowSaving.saveAsNewWorkflow();
|
||||
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
||||
},
|
||||
|
|
@ -317,14 +324,12 @@ export function handleExecutionFinishedWithWaitTill(
|
|||
export function handleExecutionFinishedWithErrorOrCanceled(
|
||||
execution: SimplifiedExecution,
|
||||
runExecutionData: IRunExecutionData,
|
||||
documentId: WorkflowDocumentId,
|
||||
) {
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const documentTitle = useDocumentTitle();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
|
||||
|
|
@ -348,7 +353,7 @@ export function handleExecutionFinishedWithErrorOrCanceled(
|
|||
workflowHelpers.getNodeTypes(),
|
||||
).nodeGraph,
|
||||
),
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
workflow_id: workflowDocumentStore.workflowId,
|
||||
};
|
||||
|
||||
if (
|
||||
|
|
@ -397,12 +402,12 @@ export function handleExecutionFinishedWithErrorOrCanceled(
|
|||
function handleExecutionFinishedSuccessfully(
|
||||
workflowName: string,
|
||||
message: string,
|
||||
workflowState: WorkflowState,
|
||||
documentId: WorkflowDocumentId,
|
||||
) {
|
||||
const toast = useToast();
|
||||
|
||||
useDocumentTitle().setDocumentTitle(workflowName, 'IDLE');
|
||||
workflowState.setActiveExecutionId(undefined);
|
||||
useWorkflowExecutionStateStore(documentId).setActiveExecutionId(undefined);
|
||||
toast.showMessage({
|
||||
title: message,
|
||||
type: 'success',
|
||||
|
|
@ -413,7 +418,7 @@ function handleExecutionFinishedSuccessfully(
|
|||
* Handle the case when the workflow execution finished successfully.
|
||||
*/
|
||||
export function handleExecutionFinishedWithSuccessOrOther(
|
||||
workflowState: WorkflowState,
|
||||
documentId: WorkflowDocumentId,
|
||||
executionStatus: ExecutionStatus,
|
||||
successToastAlreadyShown: boolean,
|
||||
) {
|
||||
|
|
@ -422,9 +427,7 @@ export function handleExecutionFinishedWithSuccessOrOther(
|
|||
const i18n = useI18n();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const workflowName = workflowDocumentStore.name;
|
||||
|
||||
useDocumentTitle().setDocumentTitle(workflowName, 'IDLE');
|
||||
|
|
@ -460,14 +463,14 @@ export function handleExecutionFinishedWithSuccessOrOther(
|
|||
handleExecutionFinishedSuccessfully(
|
||||
workflowName,
|
||||
i18n.baseText('pushConnection.nodeExecutedSuccessfully'),
|
||||
workflowState,
|
||||
documentId,
|
||||
);
|
||||
}
|
||||
} else if (!successToastAlreadyShown) {
|
||||
handleExecutionFinishedSuccessfully(
|
||||
workflowName,
|
||||
i18n.baseText('pushConnection.workflowExecutedSuccessfully'),
|
||||
workflowState,
|
||||
documentId,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -481,16 +484,13 @@ export function handleExecutionFinishedWithSuccessOrOther(
|
|||
export function setRunExecutionData(
|
||||
execution: SimplifiedExecution,
|
||||
runExecutionData: IRunExecutionData,
|
||||
workflowState: WorkflowState,
|
||||
documentId: WorkflowDocumentId,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const runDataExecutedErrorMessage = getRunDataExecutedErrorMessage(execution);
|
||||
const runDataExecutedErrorMessage = getRunDataExecutedErrorMessage(execution, documentId);
|
||||
|
||||
workflowState.executingNode.clearNodeExecutionQueue();
|
||||
workflowExecutionStateStore.executingNode.clearNodeExecutionQueue();
|
||||
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId(execution.id));
|
||||
const workflowExecution = executionDataStore.getExecutionSnapshot();
|
||||
|
|
|
|||
|
|
@ -8,30 +8,27 @@ import {
|
|||
handleExecutionFinishedWithWaitTill,
|
||||
setRunExecutionData,
|
||||
} from './executionFinished';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.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';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function executionRecovered(
|
||||
{ data }: ExecutionRecovered,
|
||||
options: { router: ReturnType<typeof useRouter>; workflowState: WorkflowState },
|
||||
options: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const { documentId } = options;
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const uiStore = useUIStore();
|
||||
|
||||
// No workflow is actively running, therefore we ignore this event
|
||||
if (typeof workflowExecutionStateStore.activeExecutionId === 'undefined') {
|
||||
// Only recover the execution this document is tracking. A mismatch (including
|
||||
// the no-active-execution case, where activeExecutionId is undefined) means
|
||||
// the event belongs to another execution and must be ignored.
|
||||
if (workflowExecutionStateStore.activeExecutionId !== data.executionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.setProcessingExecutionResults(true);
|
||||
|
||||
const execution = await fetchExecutionData(data.executionId);
|
||||
const execution = await fetchExecutionData(data.executionId, documentId);
|
||||
if (!execution) {
|
||||
uiStore.setProcessingExecutionResults(false);
|
||||
return;
|
||||
|
|
@ -43,10 +40,10 @@ export async function executionRecovered(
|
|||
if (execution.data?.waitTill !== undefined) {
|
||||
handleExecutionFinishedWithWaitTill(execution.workflowId ?? '', options);
|
||||
} else if (execution.status === 'error' || execution.status === 'canceled') {
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, documentId);
|
||||
} else {
|
||||
handleExecutionFinishedWithSuccessOrOther(options.workflowState, execution.status, false);
|
||||
handleExecutionFinishedWithSuccessOrOther(documentId, execution.status, false);
|
||||
}
|
||||
|
||||
setRunExecutionData(execution, runExecutionData, options.workflowState);
|
||||
setRunExecutionData(execution, runExecutionData, documentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { Router } from 'vue-router';
|
||||
import { executionStarted } from './executionStarted';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
|
|
@ -8,35 +9,34 @@ import {
|
|||
import type { ExecutionStarted } from '@n8n/api-types/push/execution';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
describe('executionStarted', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
const documentId = createWorkflowDocumentId('wf-123');
|
||||
let options: PushHandlerOptions;
|
||||
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
|
||||
function makeEvent(executionId = 'exec-1'): ExecutionStarted {
|
||||
function makeEvent(executionId = 'exec-1', workflowId = 'wf-123'): ExecutionStarted {
|
||||
return {
|
||||
type: 'executionStarted',
|
||||
data: { executionId } as ExecutionStarted['data'],
|
||||
data: { executionId, workflowId } as ExecutionStarted['data'],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId('wf-123');
|
||||
options = { router: mock<Router>(), documentId };
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('wf-123'));
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
workflowDocumentStore.setName('My Workflow');
|
||||
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('wf-123'),
|
||||
);
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
});
|
||||
|
||||
it('should skip when activeExecutionId is undefined', async () => {
|
||||
// activeExecutionId defaults to undefined, no need to set it
|
||||
await executionStarted(makeEvent());
|
||||
await executionStarted(makeEvent(), options);
|
||||
|
||||
// workflowExecutionStateStore.activeExecutionId should remain undefined
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined();
|
||||
|
|
@ -49,7 +49,7 @@ describe('executionStarted', () => {
|
|||
it('should accept execution when activeExecutionId is null and populate workflowData from store', async () => {
|
||||
workflowExecutionStateStore.setActiveExecutionId(null);
|
||||
|
||||
await executionStarted(makeEvent('exec-1'));
|
||||
await executionStarted(makeEvent('exec-1'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1');
|
||||
|
||||
|
|
@ -61,6 +61,26 @@ describe('executionStarted', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should skip when the event workflow id does not match the document', async () => {
|
||||
// A pending run is staged for this document...
|
||||
workflowExecutionStateStore.setActiveExecutionId(null);
|
||||
|
||||
// ...but the event belongs to a different workflow (e.g. a concurrent
|
||||
// scheduled run). It must not hijack this document's pending slot.
|
||||
await executionStarted(makeEvent('exec-9', 'other-wf'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBeNull();
|
||||
expect(useExecutionDataStore(createExecutionDataId('exec-9')).execution).toBeNull();
|
||||
});
|
||||
|
||||
it('should accept when the event workflow id matches the document', async () => {
|
||||
workflowExecutionStateStore.setActiveExecutionId(null);
|
||||
|
||||
await executionStarted(makeEvent('exec-1', 'wf-123'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1');
|
||||
});
|
||||
|
||||
it('should not reinitialize when same execution ID arrives', async () => {
|
||||
// Set up an active execution with existing data
|
||||
workflowExecutionStateStore.promotePendingExecution('exec-1');
|
||||
|
|
@ -78,7 +98,7 @@ describe('executionStarted', () => {
|
|||
|
||||
const executionBefore = executionDataStore.execution;
|
||||
|
||||
await executionStarted(makeEvent('exec-1'));
|
||||
await executionStarted(makeEvent('exec-1'), options);
|
||||
|
||||
// workflowExecutionStateStore.activeExecutionId should remain 'exec-1' without change
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1');
|
||||
|
|
@ -111,7 +131,7 @@ describe('executionStarted', () => {
|
|||
|
||||
it('should accept execution when activeExecutionId is undefined in iframe (post-executionFinished)', async () => {
|
||||
// activeExecutionId defaults to undefined; in iframe context this should still accept
|
||||
await executionStarted(makeEvent('exec-2'));
|
||||
await executionStarted(makeEvent('exec-2'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-2');
|
||||
|
||||
|
|
@ -139,7 +159,7 @@ describe('executionStarted', () => {
|
|||
} as never,
|
||||
});
|
||||
|
||||
await executionStarted(makeEvent('exec-2'));
|
||||
await executionStarted(makeEvent('exec-2'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-2');
|
||||
|
||||
|
|
@ -165,7 +185,7 @@ describe('executionStarted', () => {
|
|||
data: { resultData: { runData: {} } } as never,
|
||||
});
|
||||
|
||||
await executionStarted(makeEvent('exec-1'));
|
||||
await executionStarted(makeEvent('exec-1'), options);
|
||||
|
||||
// Should remain exec-1 without reinitializing
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBe('exec-1');
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
import type { ExecutionStarted } from '@n8n/api-types/push/execution';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.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';
|
||||
import type { IRunExecutionData } from 'n8n-workflow';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'executionStarted' event, which happens when a workflow is executed.
|
||||
*/
|
||||
export async function executionStarted({ data }: ExecutionStarted) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
workflowDocumentStore.documentId,
|
||||
);
|
||||
export async function executionStarted(
|
||||
{ data }: ExecutionStarted,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const isIframe = window !== window.parent;
|
||||
|
||||
// A single push connection serves the active document, so a concurrent
|
||||
// execution of a *different* workflow (e.g. a scheduled run firing while this
|
||||
// document has a pending run) would otherwise hijack this document's pending
|
||||
// (null) execution slot. Reject events for another workflow. The iframe/demo
|
||||
// path is exempt: it only ever receives events relayed for the workflow it
|
||||
// previews, and its document id may not carry a comparable workflow id.
|
||||
if (!isIframe && data.workflowId !== workflowExecutionStateStore.workflowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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).
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { nodeExecuteAfter } from './nodeExecuteAfter';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useAssistantStore } from '@/features/ai/assistant/assistant.store';
|
||||
import type { NodeExecuteAfter } from '@n8n/api-types/push/execution';
|
||||
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 type { Router } from 'vue-router';
|
||||
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';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
vi.mock('@/features/ai/assistant/assistant.store', () => ({
|
||||
useAssistantStore: vi.fn().mockReturnValue({
|
||||
|
|
@ -27,8 +26,8 @@ vi.mock('@/features/execution/executions/executions.utils', async (importOrigina
|
|||
import { openFormPopupWindow } from '@/features/execution/executions/executions.utils';
|
||||
|
||||
describe('nodeExecuteAfter', () => {
|
||||
let mockOptions: { workflowState: Mocked<WorkflowState> };
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
const documentId = createWorkflowDocumentId('test-wf');
|
||||
let options: PushHandlerOptions;
|
||||
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
|
||||
|
||||
|
|
@ -36,12 +35,10 @@ describe('nodeExecuteAfter', () => {
|
|||
vi.mocked(openFormPopupWindow).mockClear();
|
||||
setActivePinia(createPinia());
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId('test-wf');
|
||||
options = { router: mock<Router>(), documentId };
|
||||
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('test-wf'),
|
||||
);
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
vi.spyOn(workflowExecutionStateStore.executingNode, 'removeExecutingNode');
|
||||
|
||||
executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1'));
|
||||
executionDataStore.setExecution(
|
||||
|
|
@ -54,14 +51,6 @@ describe('nodeExecuteAfter', () => {
|
|||
);
|
||||
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-1');
|
||||
|
||||
mockOptions = {
|
||||
workflowState: mock<WorkflowState>({
|
||||
executingNode: {
|
||||
removeExecutingNode: vi.fn(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it('should update node execution data with placeholder and remove executing node', async () => {
|
||||
|
|
@ -82,10 +71,10 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
expect(mockOptions.workflowState.executingNode.removeExecutingNode).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.workflowState.executingNode.removeExecutingNode).toHaveBeenCalledWith(
|
||||
expect(workflowExecutionStateStore.executingNode.removeExecutingNode).toHaveBeenCalledTimes(1);
|
||||
expect(workflowExecutionStateStore.executingNode.removeExecutingNode).toHaveBeenCalledWith(
|
||||
'Test Node',
|
||||
);
|
||||
expect(assistantStore.onNodeExecution).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -102,6 +91,36 @@ describe('nodeExecuteAfter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should skip when the execution id does not match the active execution', async () => {
|
||||
const assistantStore = useAssistantStore();
|
||||
// onNodeExecution is a shared module-level mock; reset its call history so
|
||||
// this assertion only reflects the call (if any) from this test.
|
||||
vi.mocked(assistantStore.onNodeExecution).mockClear();
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'other-exec',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: { main: [1] },
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
// Nothing belonging to the active execution should be touched.
|
||||
expect(workflowExecutionStateStore.executingNode.removeExecutingNode).not.toHaveBeenCalled();
|
||||
expect(assistantStore.onNodeExecution).not.toHaveBeenCalled();
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
expect(runData?.['Test Node']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple connection types', async () => {
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
|
|
@ -122,7 +141,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
expect(runData?.['Test Node'][0].data).toEqual({
|
||||
|
|
@ -155,7 +174,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
expect(runData?.['Test Node'][0].data).toEqual({
|
||||
|
|
@ -179,7 +198,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
const taskData = runData?.['Test Node'][0];
|
||||
|
|
@ -216,7 +235,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
// Should only contain main connection, invalid_connection should be filtered out
|
||||
|
|
@ -247,7 +266,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
expect(openFormPopupWindow).toHaveBeenCalledWith(formUrl);
|
||||
});
|
||||
|
|
@ -269,7 +288,7 @@ describe('nodeExecuteAfter', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event, mockOptions);
|
||||
await nodeExecuteAfter(event, options);
|
||||
|
||||
expect(openFormPopupWindow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
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 { 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';
|
||||
|
|
@ -10,21 +8,26 @@ import type { PushPayload } from '@n8n/api-types';
|
|||
import { isValidNodeConnectionType } from '@/app/utils/typeGuards';
|
||||
import { openFormPopupWindow } from '@/features/execution/executions/executions.utils';
|
||||
import { trackNodeExecution } from './trackNodeExecution';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'nodeExecuteAfter' event, which happens after a node is executed.
|
||||
*/
|
||||
export async function nodeExecuteAfter(
|
||||
{ data: pushData }: NodeExecuteAfter,
|
||||
{ workflowState }: { workflowState: WorkflowState },
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const assistantStore = useAssistantStore();
|
||||
|
||||
// Ignore node events that don't belong to the execution this document is
|
||||
// tracking — a concurrent execution's node must not write into this
|
||||
// document's data or fire its side effects (form popups, tracking, assistant).
|
||||
const activeExecutionId = workflowExecutionStateStore.activeExecutionId;
|
||||
if (activeExecutionId !== pushData.executionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We trim the actual data returned from the node execution to avoid performance issues
|
||||
* when dealing with large datasets. Instead of storing the actual data, we initially store
|
||||
|
|
@ -58,24 +61,24 @@ export async function nodeExecuteAfter(
|
|||
},
|
||||
};
|
||||
|
||||
const activeExecutionId = workflowExecutionStateStore.activeExecutionId;
|
||||
if (typeof activeExecutionId === 'string') {
|
||||
useExecutionDataStore(createExecutionDataId(activeExecutionId)).updateNodeExecutionStatus(
|
||||
pushDataWithPlaceholderOutputData,
|
||||
);
|
||||
useExecutionDataStore(createExecutionDataId(pushData.executionId)).updateNodeExecutionStatus(
|
||||
pushDataWithPlaceholderOutputData,
|
||||
);
|
||||
|
||||
if (pushDataWithPlaceholderOutputData.data.executionStatus !== 'waiting') {
|
||||
void trackNodeExecution(pushDataWithPlaceholderOutputData, workflowsStore.workflowId);
|
||||
}
|
||||
if (pushDataWithPlaceholderOutputData.data.executionStatus !== 'waiting') {
|
||||
void trackNodeExecution(
|
||||
pushDataWithPlaceholderOutputData,
|
||||
workflowExecutionStateStore.workflowId,
|
||||
);
|
||||
}
|
||||
|
||||
workflowState.executingNode.removeExecutingNode(pushData.nodeName);
|
||||
workflowExecutionStateStore.executingNode.removeExecutingNode(pushData.nodeName);
|
||||
|
||||
// Side effects
|
||||
if (pushData.data.executionStatus === 'waiting' && pushData.data.metadata?.resumeFormUrl) {
|
||||
openFormPopupWindow(pushData.data.metadata.resumeFormUrl);
|
||||
} else if (pushData.data.executionStatus !== 'waiting') {
|
||||
void trackNodeExecution(pushData, workflowsStore.workflowId);
|
||||
void trackNodeExecution(pushData, workflowExecutionStateStore.workflowId);
|
||||
}
|
||||
|
||||
void assistantStore.onNodeExecution(pushData);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { Router } from 'vue-router';
|
||||
import { nodeExecuteAfterData } from './nodeExecuteAfterData';
|
||||
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 { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
describe('nodeExecuteAfterData', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
const documentId = createWorkflowDocumentId('test-wf');
|
||||
let options: PushHandlerOptions;
|
||||
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId('test-wf');
|
||||
options = { router: mock<Router>(), documentId };
|
||||
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('test-wf'),
|
||||
);
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
executionDataStore = useExecutionDataStore(createExecutionDataId('exec-1'));
|
||||
executionDataStore.setExecution(
|
||||
|
|
@ -71,7 +71,7 @@ describe('nodeExecuteAfterData', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfterData(event);
|
||||
await nodeExecuteAfterData(event, options);
|
||||
|
||||
// The exec store's run data for 'Test Node' should now have the real data
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
|
|
@ -80,4 +80,32 @@ describe('nodeExecuteAfterData', () => {
|
|||
main: [[{ json: { foo: 'bar' } }]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip when the execution id does not match the active execution', async () => {
|
||||
const event: NodeExecuteAfterData = {
|
||||
type: 'nodeExecuteAfterData',
|
||||
data: {
|
||||
executionId: 'other-exec',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: { main: [1] },
|
||||
data: {
|
||||
executionTime: 0,
|
||||
startTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
data: {
|
||||
main: [[{ json: { foo: 'bar' } }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfterData(event, options);
|
||||
|
||||
// The active execution (exec-1) keeps its placeholder data untouched.
|
||||
const runData = executionDataStore.execution?.data?.resultData.runData;
|
||||
expect(runData?.['Test Node'][0].data).toEqual({
|
||||
main: [[{ json: { placeholder: true } }]],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,41 @@
|
|||
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 { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'nodeExecuteAfterData' event, which is sent after a node has executed and contains the resulting data.
|
||||
*/
|
||||
export async function nodeExecuteAfterData({ data: pushData }: NodeExecuteAfterData) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
workflowDocumentStore.value.documentId,
|
||||
);
|
||||
export async function nodeExecuteAfterData(
|
||||
{ data: pushData }: NodeExecuteAfterData,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
const schemaPreviewStore = useSchemaPreviewStore();
|
||||
|
||||
// Ignore node events that don't belong to the execution this document is
|
||||
// tracking — a concurrent execution's data must not land on this document.
|
||||
const activeExecutionId = workflowExecutionStateStore.activeExecutionId;
|
||||
if (typeof activeExecutionId === 'string') {
|
||||
useExecutionDataStore(createExecutionDataId(activeExecutionId)).updateNodeExecutionRunData(
|
||||
pushData,
|
||||
);
|
||||
if (activeExecutionId !== pushData.executionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = workflowDocumentStore.value.getNodeByName(pushData.nodeName);
|
||||
useExecutionDataStore(createExecutionDataId(pushData.executionId)).updateNodeExecutionRunData(
|
||||
pushData,
|
||||
);
|
||||
|
||||
const node = workflowDocumentStore.getNodeByName(pushData.nodeName);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
void schemaPreviewStore.trackSchemaPreviewExecution(workflowsStore.workflowId, node, pushData);
|
||||
void schemaPreviewStore.trackSchemaPreviewExecution(
|
||||
workflowDocumentStore.workflowId,
|
||||
node,
|
||||
pushData,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { Router } from 'vue-router';
|
||||
import { nodeExecuteBefore } from './nodeExecuteBefore';
|
||||
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import { createTestWorkflowExecutionResponse } from '@/__tests__/mocks';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
describe('nodeExecuteBefore', () => {
|
||||
const documentId = createWorkflowDocumentId('test-wf');
|
||||
let options: PushHandlerOptions;
|
||||
let workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
|
||||
function makeEvent(executionId = 'exec-1'): NodeExecuteBefore {
|
||||
return {
|
||||
type: 'nodeExecuteBefore',
|
||||
data: {
|
||||
executionId,
|
||||
nodeName: 'Test Node',
|
||||
data: { startTime: 0, executionIndex: 0, source: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
options = { router: mock<Router>(), documentId };
|
||||
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
vi.spyOn(workflowExecutionStateStore.executingNode, 'addExecutingNode');
|
||||
|
||||
useExecutionDataStore(createExecutionDataId('exec-1')).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: 'exec-1', status: 'running' }),
|
||||
);
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-1');
|
||||
});
|
||||
|
||||
it('adds the executing node when the execution id matches the active execution', async () => {
|
||||
await nodeExecuteBefore(makeEvent('exec-1'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.executingNode.addExecutingNode).toHaveBeenCalledWith(
|
||||
'Test Node',
|
||||
);
|
||||
});
|
||||
|
||||
it('skips when the execution id does not match the active execution', async () => {
|
||||
await nodeExecuteBefore(makeEvent('other-exec'), options);
|
||||
|
||||
expect(workflowExecutionStateStore.executingNode.addExecutingNode).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,28 +1,26 @@
|
|||
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.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';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'nodeExecuteBefore' event, which happens before a node is executed.
|
||||
*/
|
||||
export async function nodeExecuteBefore(
|
||||
{ data }: NodeExecuteBefore,
|
||||
{ workflowState }: { workflowState: WorkflowState },
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
workflowState.executingNode.addExecutingNode(data.nodeName);
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
// Ignore node events that don't belong to the execution this document is
|
||||
// tracking — otherwise a concurrent execution's node would pollute this
|
||||
// document's spinner queue and execution data.
|
||||
const activeExecutionId = workflowExecutionStateStore.activeExecutionId;
|
||||
if (typeof activeExecutionId === 'string') {
|
||||
useExecutionDataStore(createExecutionDataId(activeExecutionId)).addNodeExecutionStartedData(
|
||||
data,
|
||||
);
|
||||
if (activeExecutionId !== data.executionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
workflowExecutionStateStore.executingNode.addExecutingNode(data.nodeName);
|
||||
|
||||
useExecutionDataStore(createExecutionDataId(data.executionId)).addNodeExecutionStartedData(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
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';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'testWebhookDeleted' push message, which is sent when a test webhook is deleted.
|
||||
*/
|
||||
export async function testWebhookDeleted(
|
||||
{ data }: TestWebhookDeleted,
|
||||
options: { workflowState: WorkflowState },
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
if (data.workflowId === workflowsStore.workflowId) {
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setExecutionWaitingForWebhook(false);
|
||||
options.workflowState.setActiveExecutionId(undefined);
|
||||
if (data.workflowId === workflowExecutionStateStore.workflowId) {
|
||||
workflowExecutionStateStore.setExecutionWaitingForWebhook(false);
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
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';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Handles the 'testWebhookReceived' push message, which is sent when a test webhook is received.
|
||||
*/
|
||||
export async function testWebhookReceived(
|
||||
{ data }: TestWebhookReceived,
|
||||
options: { workflowState: WorkflowState },
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(documentId);
|
||||
|
||||
if (data.workflowId === workflowsStore.workflowId) {
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setExecutionWaitingForWebhook(false);
|
||||
options.workflowState.setActiveExecutionId(data.executionId ?? null);
|
||||
if (data.workflowId === workflowExecutionStateStore.workflowId) {
|
||||
workflowExecutionStateStore.setExecutionWaitingForWebhook(false);
|
||||
workflowExecutionStateStore.setActiveExecutionId(data.executionId ?? null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import type { Router } from 'vue-router';
|
||||
import type { WorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
/**
|
||||
* Dependencies passed to push connection handlers, resolved per event in
|
||||
* `usePushConnection.processEvent`.
|
||||
*
|
||||
* - `documentId` identifies the currently-open workflow document (resolved at
|
||||
* event time from the injected workflow document store), so handlers can
|
||||
* reach the right per-document stores without reading any global "current
|
||||
* workflow" state.
|
||||
* - `router` is only needed by handlers that re-run or save the workflow
|
||||
* (`useRunWorkflow` / `useWorkflowSaving`), which cannot call `useRouter()`
|
||||
* from an async, non-setup context.
|
||||
*/
|
||||
export interface PushHandlerOptions {
|
||||
router: Router;
|
||||
documentId: WorkflowDocumentId;
|
||||
}
|
||||
|
|
@ -1,23 +1,25 @@
|
|||
import type { WorkflowActivated } from '@n8n/api-types/push/workflow';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useBannersStore } from '@/features/shared/banners/banners.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function workflowActivated({ data }: WorkflowActivated) {
|
||||
export async function workflowActivated(
|
||||
{ data }: WorkflowActivated,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const bannersStore = useBannersStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const { workflowId, activeVersionId } = data;
|
||||
|
||||
const workflowIsBeingViewed = workflowsStore.workflowId === workflowId;
|
||||
const activeVersionChanged = workflowDocumentStore?.value?.activeVersionId !== activeVersionId;
|
||||
const workflowIsBeingViewed = workflowDocumentStore.workflowId === workflowId;
|
||||
const activeVersionChanged = workflowDocumentStore.activeVersionId !== activeVersionId;
|
||||
if (workflowIsBeingViewed && activeVersionChanged) {
|
||||
// Only update workflow if there are no unsaved changes
|
||||
if (!uiStore.stateIsDirty) {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
import type { WorkflowAutoDeactivated } from '@n8n/api-types/push/workflow';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useBannersStore } from '@/features/shared/banners/banners.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function workflowAutoDeactivated({ data }: WorkflowAutoDeactivated) {
|
||||
export async function workflowAutoDeactivated(
|
||||
{ data }: WorkflowAutoDeactivated,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const bannersStore = useBannersStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
workflowsStore.setWorkflowInactive(data.workflowId);
|
||||
|
||||
if (workflowsStore.workflowId === data.workflowId) {
|
||||
if (workflowDocumentStore.workflowId === data.workflowId) {
|
||||
// Only update workflow if there are no unsaved changes
|
||||
if (!uiStore.stateIsDirty) {
|
||||
const updatedWorkflow = await workflowsListStore.fetchWorkflow(data.workflowId);
|
||||
|
|
@ -26,7 +30,7 @@ export async function workflowAutoDeactivated({ data }: WorkflowAutoDeactivated)
|
|||
// initializeWorkspace calls initState which sets the document store
|
||||
await initializeWorkspace(updatedWorkflow);
|
||||
} else {
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
}
|
||||
|
||||
bannersStore.pushBannerToStack('WORKFLOW_AUTO_DEACTIVATED');
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import type { WorkflowDeactivated } from '@n8n/api-types/push/workflow';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function workflowDeactivated({ data }: WorkflowDeactivated) {
|
||||
export async function workflowDeactivated(
|
||||
{ data }: WorkflowDeactivated,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
const uiStore = useUIStore();
|
||||
|
||||
if (workflowsStore.workflowId === data.workflowId) {
|
||||
if (workflowDocumentStore.workflowId === data.workflowId) {
|
||||
// Only update workflow if there are no unsaved changes
|
||||
if (!uiStore.stateIsDirty) {
|
||||
const updatedWorkflow = await workflowsListStore.fetchWorkflow(data.workflowId);
|
||||
|
|
@ -22,7 +24,7 @@ export async function workflowDeactivated({ data }: WorkflowDeactivated) {
|
|||
// initializeWorkspace calls initState which sets the document store
|
||||
await initializeWorkspace(updatedWorkflow);
|
||||
} else {
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,22 @@ import { useToast } from '@/app/composables/useToast';
|
|||
import { useActivationError } from '@/app/composables/useActivationError';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function workflowFailedToActivate({ data }: WorkflowFailedToActivate) {
|
||||
export async function workflowFailedToActivate(
|
||||
{ data }: WorkflowFailedToActivate,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
|
||||
if (workflowsStore.workflowId !== data.workflowId) {
|
||||
if (workflowDocumentStore.workflowId !== data.workflowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
workflowsStore.setWorkflowInactive(data.workflowId);
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { Router } from 'vue-router';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
||||
import type { WorkflowSettingsUpdated } from '@n8n/api-types/push/workflow';
|
||||
|
||||
import { workflowSettingsUpdated } from './workflowSettingsUpdated';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
|
||||
mockWorkflowDocumentStore: {
|
||||
workflowId: '',
|
||||
allNodes: [],
|
||||
name: '',
|
||||
settings: {},
|
||||
|
|
@ -38,14 +42,15 @@ const makeEvent = (
|
|||
});
|
||||
|
||||
describe('workflowSettingsUpdated', () => {
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let options: PushHandlerOptions;
|
||||
let workflowsListStore: ReturnType<typeof mockedStore<typeof useWorkflowsListStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsListStore = mockedStore(useWorkflowsListStore);
|
||||
mockWorkflowDocumentStore.workflowId = '';
|
||||
options = { router: mock<Router>(), documentId: createWorkflowDocumentId('current') };
|
||||
});
|
||||
|
||||
it('merges partial settings into an existing list entry', async () => {
|
||||
|
|
@ -57,7 +62,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
},
|
||||
} as unknown as typeof workflowsListStore.workflowsById;
|
||||
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }));
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }), options);
|
||||
|
||||
expect(workflowsListStore.workflowsById['wf-1'].settings).toEqual({
|
||||
availableInMCP: true,
|
||||
|
|
@ -70,7 +75,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
'wf-1': { id: 'wf-1', name: 'wf' },
|
||||
} as unknown as typeof workflowsListStore.workflowsById;
|
||||
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }));
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }), options);
|
||||
|
||||
expect(workflowsListStore.workflowsById['wf-1'].settings).toEqual({
|
||||
availableInMCP: true,
|
||||
|
|
@ -78,16 +83,16 @@ describe('workflowSettingsUpdated', () => {
|
|||
});
|
||||
|
||||
it('does nothing for the document store when the workflow is not the active one', async () => {
|
||||
workflowsStore.setWorkflowId('other-workflow');
|
||||
mockWorkflowDocumentStore.workflowId = 'other-workflow';
|
||||
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }));
|
||||
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }), options);
|
||||
|
||||
expect(mockWorkflowDocumentStore.mergeSettings).not.toHaveBeenCalled();
|
||||
expect(mockWorkflowDocumentStore.setChecksum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('merges settings and uses payload checksum for the active document', async () => {
|
||||
workflowsStore.setWorkflowId('wf-current');
|
||||
mockWorkflowDocumentStore.workflowId = 'wf-current';
|
||||
workflowsListStore.workflowsById = {
|
||||
'wf-current': {
|
||||
id: 'wf-current',
|
||||
|
|
@ -99,6 +104,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
|
||||
await workflowSettingsUpdated(
|
||||
makeEvent('wf-current', { availableInMCP: true }, 'fresh-checksum'),
|
||||
options,
|
||||
);
|
||||
|
||||
expect(mockWorkflowDocumentStore.mergeSettings).toHaveBeenCalledWith({ availableInMCP: true });
|
||||
|
|
@ -107,7 +113,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
});
|
||||
|
||||
it('applies settings but skips checksum refresh when none is provided', async () => {
|
||||
workflowsStore.setWorkflowId('wf-current');
|
||||
mockWorkflowDocumentStore.workflowId = 'wf-current';
|
||||
workflowsListStore.workflowsById = {
|
||||
'wf-current': {
|
||||
id: 'wf-current',
|
||||
|
|
@ -117,7 +123,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
},
|
||||
} as unknown as typeof workflowsListStore.workflowsById;
|
||||
|
||||
await workflowSettingsUpdated(makeEvent('wf-current', { availableInMCP: true }));
|
||||
await workflowSettingsUpdated(makeEvent('wf-current', { availableInMCP: true }), options);
|
||||
|
||||
expect(mockWorkflowDocumentStore.mergeSettings).toHaveBeenCalledWith({ availableInMCP: true });
|
||||
expect(mockWorkflowDocumentStore.setChecksum).not.toHaveBeenCalled();
|
||||
|
|
@ -125,7 +131,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
});
|
||||
|
||||
it('merges multiple settings keys in one event', async () => {
|
||||
workflowsStore.setWorkflowId('wf-current');
|
||||
mockWorkflowDocumentStore.workflowId = 'wf-current';
|
||||
workflowsListStore.workflowsById = {
|
||||
'wf-current': {
|
||||
id: 'wf-current',
|
||||
|
|
@ -136,6 +142,7 @@ describe('workflowSettingsUpdated', () => {
|
|||
|
||||
await workflowSettingsUpdated(
|
||||
makeEvent('wf-current', { availableInMCP: true, timezone: 'UTC' }),
|
||||
options,
|
||||
);
|
||||
|
||||
expect(mockWorkflowDocumentStore.mergeSettings).toHaveBeenCalledWith({
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import type { WorkflowSettingsUpdated } from '@n8n/api-types/push/workflow';
|
||||
import type { IWorkflowSettings } from '@/Interface';
|
||||
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import type { PushHandlerOptions } from './types';
|
||||
|
||||
export async function workflowSettingsUpdated({
|
||||
data: { workflowId, settings, checksum },
|
||||
}: WorkflowSettingsUpdated) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
export async function workflowSettingsUpdated(
|
||||
{ data: { workflowId, settings, checksum } }: WorkflowSettingsUpdated,
|
||||
{ documentId }: PushHandlerOptions,
|
||||
) {
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
|
||||
// Keep the list entry in sync so other views (workflow cards, MCP
|
||||
|
|
@ -28,10 +25,11 @@ export async function workflowSettingsUpdated({
|
|||
}
|
||||
}
|
||||
|
||||
// Only the editor tab needs to resync the document store + checksum.
|
||||
if (workflowId !== workflowsStore.workflowId) return;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
|
||||
// Only the editor tab needs to resync the document store + checksum.
|
||||
if (workflowId !== workflowDocumentStore.workflowId) return;
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
workflowDocumentStore.mergeSettings(settings);
|
||||
|
||||
if (checksum) {
|
||||
|
|
|
|||
|
|
@ -23,22 +23,14 @@ import {
|
|||
workflowAutoDeactivated,
|
||||
workflowSettingsUpdated,
|
||||
} from '@/app/composables/usePushConnection/handlers';
|
||||
import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { PushHandlerOptions } from '@/app/composables/usePushConnection/handlers/types';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { createEventQueue } from '@n8n/utils/event-queue';
|
||||
import type { useRouter } from 'vue-router';
|
||||
|
||||
export function usePushConnection({
|
||||
router,
|
||||
workflowState,
|
||||
}: {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
workflowState?: WorkflowState;
|
||||
}) {
|
||||
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||
const pushStore = usePushConnectionStore();
|
||||
const options = {
|
||||
router,
|
||||
workflowState: workflowState ?? injectWorkflowState(),
|
||||
};
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const { enqueue } = createEventQueue<PushMessage>(processEvent);
|
||||
|
||||
|
|
@ -60,6 +52,13 @@ export function usePushConnection({
|
|||
* Process received push message event by calling the correct handler
|
||||
*/
|
||||
async function processEvent(event: PushMessage) {
|
||||
// Resolve the current workflow document per event so handlers always act on
|
||||
// the workflow the user is currently viewing, even as they navigate.
|
||||
const options: PushHandlerOptions = {
|
||||
router,
|
||||
documentId: workflowDocumentStore.value.documentId,
|
||||
};
|
||||
|
||||
switch (event.type) {
|
||||
case 'testWebhookDeleted':
|
||||
return await testWebhookDeleted(event, options);
|
||||
|
|
@ -76,27 +75,27 @@ export function usePushConnection({
|
|||
case 'nodeExecuteAfter':
|
||||
return await nodeExecuteAfter(event, options);
|
||||
case 'nodeExecuteAfterData':
|
||||
return await nodeExecuteAfterData(event);
|
||||
return await nodeExecuteAfterData(event, options);
|
||||
case 'executionStarted':
|
||||
return await executionStarted(event);
|
||||
return await executionStarted(event, options);
|
||||
case 'sendWorkerStatusMessage':
|
||||
return await sendWorkerStatusMessage(event);
|
||||
case 'sendConsoleMessage':
|
||||
return await sendConsoleMessage(event);
|
||||
case 'workflowFailedToActivate':
|
||||
return await workflowFailedToActivate(event);
|
||||
return await workflowFailedToActivate(event, options);
|
||||
case 'executionFinished':
|
||||
return await executionFinished(event, options);
|
||||
case 'executionRecovered':
|
||||
return await executionRecovered(event, options);
|
||||
case 'workflowActivated':
|
||||
return await workflowActivated(event);
|
||||
return await workflowActivated(event, options);
|
||||
case 'workflowDeactivated':
|
||||
return await workflowDeactivated(event);
|
||||
return await workflowDeactivated(event, options);
|
||||
case 'workflowAutoDeactivated':
|
||||
return await workflowAutoDeactivated(event);
|
||||
return await workflowAutoDeactivated(event, options);
|
||||
case 'workflowSettingsUpdated':
|
||||
return await workflowSettingsUpdated(event);
|
||||
return await workflowSettingsUpdated(event, options);
|
||||
case 'updateBuilderCredits':
|
||||
return await builderCreditsUpdated(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,11 @@ import * as workflowHelpers from './useWorkflowHelpers';
|
|||
import { renderComponent } from '@/__tests__/render';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { injectWorkflowState, useWorkflowState, type WorkflowState } from './useWorkflowState';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
async function renderTestComponent(...options: Parameters<typeof useResolvedExpression>) {
|
||||
let resolvedExpression!: ReturnType<typeof useResolvedExpression>;
|
||||
|
||||
|
|
@ -44,15 +35,10 @@ const mockResolveExpression = () => {
|
|||
return mock;
|
||||
};
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
describe('useResolvedExpression', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
vi.useFakeTimers();
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,6 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
|
||||
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import { useChat } from '@n8n/chat/composables';
|
||||
import type { INodeUi, IStartRunData } from '@/Interface';
|
||||
|
|
@ -265,16 +260,6 @@ vi.mock('vue-router', async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
describe('useRunWorkflow({ router })', () => {
|
||||
let pushConnectionStore: ReturnType<typeof usePushConnectionStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
|
@ -282,6 +267,11 @@ describe('useRunWorkflow({ router })', () => {
|
|||
let router: ReturnType<typeof useRouter>;
|
||||
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
|
||||
let agentRequestStore: ReturnType<typeof useAgentRequestStore>;
|
||||
// Production resolves the execution-state store keyed by the injected document
|
||||
// store's `documentId` ('123@latest'). `createWorkflowDocumentId` is mocked to
|
||||
// `${id}@latest`, so resolving by '123' returns the same store instance the
|
||||
// composable writes to (Pinia dedupes by id).
|
||||
let executionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
|
|
@ -292,9 +282,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
uiStore = useUIStore();
|
||||
workflowsStore = useWorkflowsStore();
|
||||
agentRequestStore = useAgentRequestStore();
|
||||
|
||||
workflowState = vi.mocked(useWorkflowState());
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
executionStateStore = useWorkflowExecutionStateStore(createWorkflowDocumentId('123'));
|
||||
|
||||
router = useRouter();
|
||||
workflowHelpers = useWorkflowHelpers();
|
||||
|
|
@ -314,7 +302,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
workflowState.setActiveExecutionId(undefined);
|
||||
executionStateStore.setActiveExecutionId(undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
@ -329,24 +317,8 @@ describe('useRunWorkflow({ router })', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should use the passed-in workflowState when injection is unavailable', async () => {
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(undefined as unknown as WorkflowState);
|
||||
|
||||
const setActiveExecutionId = vi.spyOn(workflowState, 'setActiveExecutionId');
|
||||
const { runWorkflowApi } = useRunWorkflow({ router, workflowState });
|
||||
|
||||
vi.mocked(pushConnectionStore).isConnected = true;
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue({
|
||||
executionId: '123',
|
||||
waitingForWebhook: false,
|
||||
});
|
||||
|
||||
await expect(runWorkflowApi({} as IStartRunData)).resolves.not.toThrow();
|
||||
expect(setActiveExecutionId).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully run a workflow', async () => {
|
||||
const setActiveExecutionId = vi.spyOn(workflowState, 'setActiveExecutionId');
|
||||
const setActiveExecutionId = vi.spyOn(executionStateStore, 'setActiveExecutionId');
|
||||
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||
|
||||
vi.mocked(pushConnectionStore).isConnected = true;
|
||||
|
|
@ -378,7 +350,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
});
|
||||
|
||||
it('should handle workflow run failure', async () => {
|
||||
const setActiveExecutionId = vi.spyOn(workflowState, 'setActiveExecutionId');
|
||||
const setActiveExecutionId = vi.spyOn(executionStateStore, 'setActiveExecutionId');
|
||||
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||
|
||||
vi.mocked(pushConnectionStore).isConnected = true;
|
||||
|
|
@ -407,7 +379,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
describe('runWorkflow()', () => {
|
||||
it('should return undefined if UI action "workflowRunning" is active', async () => {
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
workflowState.setActiveExecutionId('123');
|
||||
executionStateStore.setActiveExecutionId('123');
|
||||
const result = await runWorkflow({});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
|
@ -501,6 +473,87 @@ describe('useRunWorkflow({ router })', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('scoped workflow document store (eval-loop reruns)', () => {
|
||||
// Async push handlers (e.g. evaluation-loop reruns) pass their own document
|
||||
// store. The run must target that document, not whatever workflow happens to
|
||||
// be globally current — otherwise navigating away mid-evaluation reruns the
|
||||
// wrong workflow.
|
||||
function createScopedDocumentStore(workflowId: string) {
|
||||
return shallowRef({
|
||||
...mockDocumentStore,
|
||||
documentId: createWorkflowDocumentId(workflowId),
|
||||
workflowId,
|
||||
name: `Workflow ${workflowId}`,
|
||||
serialize: vi.fn().mockReturnValue({
|
||||
id: workflowId,
|
||||
name: `Workflow ${workflowId}`,
|
||||
active: false,
|
||||
nodes: [],
|
||||
pinData: {},
|
||||
} as unknown as WorkflowData),
|
||||
} as unknown as ReturnType<typeof useWorkflowDocumentStore>);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(pushConnectionStore).isConnected = true;
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue({ executionId: 'exec-1' });
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = null;
|
||||
// The injected (globally-current) document store is workflow '123'. The
|
||||
// scoped store passed per-call is a *different* workflow ('456'), so any
|
||||
// fallback to global state surfaces as a wrong id in the assertions below.
|
||||
// We deliberately leave `workflowsStore.workflowId` at its '123' default so
|
||||
// later tests that rely on the global id aren't affected.
|
||||
mockDocumentStore.serialize.mockReturnValue({
|
||||
id: '123',
|
||||
name: 'Globally current workflow',
|
||||
active: false,
|
||||
nodes: [],
|
||||
pinData: {},
|
||||
} as unknown as WorkflowData);
|
||||
});
|
||||
|
||||
it('stages execution and start data with the scoped document workflow id, not the global one', async () => {
|
||||
vi.mocked(workflowsStore).isWorkflowSaved = { '123': true, '456': true };
|
||||
// The scoped run targets workflow '456', so the execution-state store the
|
||||
// composable writes to is keyed by that document, not the global '123' one.
|
||||
const scopedExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('456'),
|
||||
);
|
||||
const setWorkflowExecutionData = vi.spyOn(
|
||||
scopedExecutionStateStore,
|
||||
'setWorkflowExecutionData',
|
||||
);
|
||||
const dataCaptor = captor();
|
||||
|
||||
const { runWorkflow } = useRunWorkflow({
|
||||
router,
|
||||
workflowDocumentStore: createScopedDocumentStore('456'),
|
||||
});
|
||||
await runWorkflow({});
|
||||
|
||||
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workflowId: '456' }),
|
||||
);
|
||||
expect(setWorkflowExecutionData).toHaveBeenCalledWith(dataCaptor);
|
||||
expect(dataCaptor.value).toMatchObject({ workflowData: { id: '456' } });
|
||||
});
|
||||
|
||||
it('saves the scoped workflow (not the global one) when a save is required before running', async () => {
|
||||
// Scoped '456' is not yet saved -> save required; global '123' is saved, so a
|
||||
// fallback to the global id would wrongly skip the save entirely.
|
||||
vi.mocked(workflowsStore).isWorkflowSaved = { '123': true };
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
|
||||
const { runWorkflow } = useRunWorkflow({
|
||||
router,
|
||||
workflowDocumentStore: createScopedDocumentStore('456'),
|
||||
});
|
||||
await runWorkflow({});
|
||||
|
||||
expect(workflowSaving.saveCurrentWorkflow).toHaveBeenCalledWith({ id: '456' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should prevent execution and show error when binary mode is "combined" with filesystem mode "default"', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
|
|
@ -785,7 +838,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
|
||||
vi.mocked(agentRequestStore).getAgentRequest.mockReturnValue(agentRequest);
|
||||
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
// ACT
|
||||
const result = await runWorkflow({
|
||||
|
|
@ -834,7 +887,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
);
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
|
||||
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
// ACT
|
||||
const result = await runWorkflow({
|
||||
|
|
@ -881,7 +934,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
nodes: [],
|
||||
} as unknown as WorkflowData);
|
||||
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
// Simulate failed execution start
|
||||
vi.mocked(workflowsStore).runWorkflow.mockRejectedValueOnce(new Error());
|
||||
|
|
@ -1334,8 +1387,8 @@ describe('useRunWorkflow({ router })', () => {
|
|||
},
|
||||
} as IExecutionResponse['data'],
|
||||
};
|
||||
const setActiveExecutionIdSpy = vi.spyOn(workflowState, 'setActiveExecutionId');
|
||||
const setWorkflowExecutionDataSpy = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setActiveExecutionIdSpy = vi.spyOn(executionStateStore, 'setActiveExecutionId');
|
||||
const setWorkflowExecutionDataSpy = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
// Stop API throws because the execution already finished server-side.
|
||||
const { useExecutionsStore } = await import(
|
||||
|
|
@ -1348,7 +1401,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
// getExecution returns the canonical finished snapshot (with id).
|
||||
vi.spyOn(workflowsStore, 'getExecution').mockResolvedValue(finishedExecution);
|
||||
|
||||
workflowState.setActiveExecutionId('exec-fin');
|
||||
executionStateStore.setActiveExecutionId('exec-fin');
|
||||
|
||||
(mockDocumentStore as unknown as { getSnapshot: () => unknown }).getSnapshot = vi
|
||||
.fn()
|
||||
|
|
@ -1390,7 +1443,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
startedAt: new Date('2025-04-01T00:00:00.000Z'),
|
||||
createdAt: new Date('2025-04-01T00:00:00.000Z'),
|
||||
};
|
||||
const markStoppedSpy = vi.spyOn(workflowState, 'markExecutionAsStopped');
|
||||
const markStoppedSpy = vi.spyOn(executionStateStore, 'markExecutionAsStopped');
|
||||
const getExecutionSpy = vi.spyOn(workflowsStore, 'getExecution');
|
||||
|
||||
const { useWorkflowsListStore } = await import('@/app/stores/workflowsList.store');
|
||||
|
|
@ -1400,7 +1453,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||
workflowsListStore.activeWorkflows.length,
|
||||
'test-wf-id',
|
||||
);
|
||||
workflowState.setActiveExecutionId('test-exec-id');
|
||||
executionStateStore.setActiveExecutionId('test-exec-id');
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('123')).setExecutionWaitingForWebhook(
|
||||
false,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
BINARY_MODE_COMBINED,
|
||||
} from 'n8n-workflow';
|
||||
import { retry } from '@n8n/utils/retry';
|
||||
import { computed } from 'vue';
|
||||
import { computed, type Ref } from 'vue';
|
||||
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
|
|
@ -39,7 +39,10 @@ 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 {
|
||||
injectWorkflowDocumentStore,
|
||||
type WorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { displayForm } from '@/features/execution/executions/executions.utils';
|
||||
import { useExternalHooks } from '@/app/composables/useExternalHooks';
|
||||
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
|
||||
|
|
@ -57,14 +60,19 @@ import { useCanvasOperations } from './useCanvasOperations';
|
|||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
|
||||
import { useWorkflowSaving } from './useWorkflowSaving';
|
||||
import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useDocumentTitle } from './useDocumentTitle';
|
||||
import { useChat } from '@n8n/chat/composables';
|
||||
import type { WorkflowObjectAccessors } from '../types';
|
||||
|
||||
export function useRunWorkflow(useRunWorkflowOpts: {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
workflowState?: WorkflowState;
|
||||
/**
|
||||
* Binds this instance to a specific workflow document. Pass this from
|
||||
* async, non-setup callers (e.g. push handlers) where `inject()` can't
|
||||
* resolve the current document; otherwise it falls back to the injected
|
||||
* document store for normal setup-context callers.
|
||||
*/
|
||||
workflowDocumentStore?: Readonly<Ref<WorkflowDocumentStore>>;
|
||||
}) {
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const i18n = useI18n();
|
||||
|
|
@ -78,14 +86,11 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
const rootStore = useRootStore();
|
||||
const pushConnectionStore = usePushConnectionStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore =
|
||||
useRunWorkflowOpts.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 nodeHelpers = useNodeHelpers();
|
||||
const workflowSaving = useWorkflowSaving({
|
||||
router: useRunWorkflowOpts.router,
|
||||
|
|
@ -118,13 +123,13 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
}
|
||||
|
||||
// Set the execution as started, but still waiting for the execution to be retrieved
|
||||
workflowState.setActiveExecutionId(null);
|
||||
workflowExecutionState.value.setActiveExecutionId(null);
|
||||
|
||||
let response: IExecutionPushResponse;
|
||||
try {
|
||||
response = await workflowsStore.runWorkflow(runData);
|
||||
} catch (error) {
|
||||
workflowState.setActiveExecutionId(undefined);
|
||||
workflowExecutionState.value.setActiveExecutionId(undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +137,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
workflowExecutionState.value.previousExecutionId !== response.executionId;
|
||||
const workflowExecutionIdIsPending = workflowExecutionState.value.activeExecutionId === null;
|
||||
if (response.executionId && workflowExecutionIdIsNew && workflowExecutionIdIsPending) {
|
||||
workflowState.setActiveExecutionId(response.executionId);
|
||||
workflowExecutionState.value.setActiveExecutionId(response.executionId);
|
||||
}
|
||||
|
||||
if (response.waitingForWebhook === true) {
|
||||
|
|
@ -169,9 +174,9 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
|
||||
const runData = workflowsStore.getWorkflowRunData;
|
||||
|
||||
const isNewWorkflow = !workflowsStore.isWorkflowSaved[workflowsStore.workflowId];
|
||||
const isNewWorkflow = !workflowsStore.isWorkflowSaved[workflowDocumentStore.value.workflowId];
|
||||
if (isNewWorkflow || (uiStore.stateIsDirty && settingsStore.isAutosaveEnabled)) {
|
||||
await workflowSaving.saveCurrentWorkflow();
|
||||
await workflowSaving.saveCurrentWorkflow({ id: workflowDocumentStore.value.workflowId });
|
||||
}
|
||||
|
||||
const workflowData = workflowDocumentStore.value.serialize();
|
||||
|
|
@ -405,7 +410,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
},
|
||||
}),
|
||||
workflowData: {
|
||||
id: workflowsStore.workflowId,
|
||||
id: workflowDocumentStore.value.workflowId,
|
||||
name: workflowData.name!,
|
||||
active: workflowData.active!,
|
||||
createdAt: 0,
|
||||
|
|
@ -413,7 +418,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
...workflowData,
|
||||
} as IWorkflowDb,
|
||||
};
|
||||
workflowState.setWorkflowExecutionData(executionData);
|
||||
workflowExecutionState.value.setWorkflowExecutionData(executionData);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
|
||||
useDocumentTitle().setDocumentTitle(workflowDocumentStore.value.name, 'EXECUTING');
|
||||
|
|
@ -461,7 +466,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
return runWorkflowApiResponse;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
workflowState.setWorkflowExecutionData(null);
|
||||
workflowExecutionState.value.setWorkflowExecutionData(null);
|
||||
useDocumentTitle().setDocumentTitle(workflowDocumentStore.value.name, 'ERROR');
|
||||
toast.showError(error, i18n.baseText('workflowRun.showError.title'));
|
||||
return undefined;
|
||||
|
|
@ -553,8 +558,8 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
} as IExecutionResponse;
|
||||
// Clear the active id so setWorkflowExecutionData's else branch sets
|
||||
// displayedExecutionId to the freshly-fetched finished id.
|
||||
workflowState.setActiveExecutionId(undefined);
|
||||
workflowState.setWorkflowExecutionData(executedData);
|
||||
workflowExecutionState.value.setActiveExecutionId(undefined);
|
||||
workflowExecutionState.value.setWorkflowExecutionData(executedData);
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.title'),
|
||||
message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.message'),
|
||||
|
|
@ -569,7 +574,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
async () => {
|
||||
const execution = await workflowsStore.getExecution(executionId);
|
||||
if (!['running', 'waiting'].includes(execution?.status as string)) {
|
||||
workflowState.markExecutionAsStopped(stopData);
|
||||
workflowExecutionState.value.markExecutionAsStopped(stopData);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -580,7 +585,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
|||
);
|
||||
|
||||
if (!markedAsStopped) {
|
||||
workflowState.markExecutionAsStopped(stopData);
|
||||
workflowExecutionState.value.markExecutionAsStopped(stopData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { render } from '@testing-library/vue';
|
|||
|
||||
import { useWorkflowInitialization } from './useWorkflowInitialization';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
|
||||
const mockSetDocumentTitle = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -139,7 +138,7 @@ function renderWithComposable(
|
|||
) {
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const init = useWorkflowInitialization({} as unknown as WorkflowState);
|
||||
const init = useWorkflowInitialization();
|
||||
callback(init);
|
||||
return () => h('div');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
|||
import { useExecutionDebugging } from '@/features/execution/executions/composables/useExecutionDebugging';
|
||||
import { getSampleWorkflowByTemplateId } from '@/features/workflows/templates/utils/workflowSamples';
|
||||
import { EnterpriseEditionFeature, VIEWS } from '@/app/constants';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
|
|
@ -38,7 +37,7 @@ import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
|||
import { injectStrict } from '@/app/utils/injectStrict';
|
||||
import { useWorkflowId } from '@/app/composables/useWorkflowId';
|
||||
|
||||
export function useWorkflowInitialization(workflowState: WorkflowState) {
|
||||
export function useWorkflowInitialization() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const i18n = useI18n();
|
||||
|
|
@ -72,9 +71,7 @@ export function useWorkflowInitialization(workflowState: WorkflowState) {
|
|||
openWorkflowTemplate,
|
||||
openWorkflowTemplateFromJSON,
|
||||
} = useCanvasOperations();
|
||||
// Pass workflowState to useExecutionDebugging since we're in the same component
|
||||
// that provides WorkflowStateKey (WorkflowLayout), so inject won't work
|
||||
const { applyExecutionData } = useExecutionDebugging(workflowState);
|
||||
const { applyExecutionData } = useExecutionDebugging();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const initializedWorkflowId = ref<string | undefined>();
|
||||
|
|
|
|||
|
|
@ -1,292 +0,0 @@
|
|||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowState, type WorkflowState } from './useWorkflowState';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { createTestTaskData, createTestWorkflowExecutionResponse } from '@/__tests__/mocks';
|
||||
import { createRunExecutionData } from 'n8n-workflow';
|
||||
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 workflowExecutionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId('test-wf');
|
||||
workflowState = useWorkflowState();
|
||||
|
||||
workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('test-wf'),
|
||||
);
|
||||
});
|
||||
|
||||
describe('markExecutionAsStopped', () => {
|
||||
beforeEach(() => {
|
||||
// Set up active execution in the facade stores
|
||||
workflowExecutionStateStore.setActiveExecutionId('test-exec-id');
|
||||
|
||||
executionDataStore = useExecutionDataStore(createExecutionDataId('test-exec-id'));
|
||||
executionDataStore.setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: 'test-exec-id',
|
||||
status: 'running',
|
||||
startedAt: new Date('2023-01-01T09:00:00Z'),
|
||||
stoppedAt: undefined,
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
createTestTaskData({ executionStatus: 'running' }),
|
||||
],
|
||||
node2: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'waiting' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove non successful node runs', () => {
|
||||
workflowState.markExecutionAsStopped();
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData?.runData;
|
||||
expect(runData?.node1).toHaveLength(1);
|
||||
expect(runData?.node1[0].executionStatus).toBe('success');
|
||||
expect(runData?.node2).toHaveLength(1);
|
||||
expect(runData?.node2[0].executionStatus).toBe('success');
|
||||
});
|
||||
|
||||
it('should update execution status, startedAt and stoppedAt when data is provided', () => {
|
||||
workflowState.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
expect(executionDataStore.execution?.status).toBe('canceled');
|
||||
expect(executionDataStore.execution?.startedAt).toEqual(new Date('2023-01-01T10:00:00Z'));
|
||||
expect(executionDataStore.execution?.stoppedAt).toEqual(new Date('2023-01-01T10:05:00Z'));
|
||||
});
|
||||
|
||||
it('should not update execution data when stopData is not provided', () => {
|
||||
workflowState.markExecutionAsStopped();
|
||||
|
||||
expect(executionDataStore.execution?.status).toBe('running');
|
||||
expect(executionDataStore.execution?.startedAt).toEqual(new Date('2023-01-01T09:00:00Z'));
|
||||
expect(executionDataStore.execution?.stoppedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('when activeExecutionId is null (pending scaffold)', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to pending state instead of the string-id default from outer beforeEach.
|
||||
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.
|
||||
workflowExecutionStateStore.setActiveExecutionId(null);
|
||||
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('filters non-success runs in the IN_PROGRESS placeholder store', () => {
|
||||
workflowState.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
const placeholder = useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID));
|
||||
expect(placeholder.execution?.data?.resultData?.runData?.node1).toHaveLength(1);
|
||||
expect(placeholder.execution?.data?.resultData?.runData?.node1[0].executionStatus).toBe(
|
||||
'success',
|
||||
);
|
||||
expect(placeholder.execution?.status).toBe('canceled');
|
||||
});
|
||||
|
||||
it('mirrors stopData onto the pendingExecution ref', () => {
|
||||
workflowState.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when activeExecutionId is undefined and displayedExecutionId is set', () => {
|
||||
beforeEach(() => {
|
||||
// Simulate post-stop-race: active was just cleared, but displayed still points
|
||||
// at the freshly-fetched finished execution.
|
||||
workflowExecutionStateStore.setActiveExecutionId('display-exec');
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
expect(workflowExecutionStateStore.activeExecutionId).toBeUndefined();
|
||||
expect(workflowExecutionStateStore.displayedExecutionId).toBe('display-exec');
|
||||
|
||||
useExecutionDataStore(createExecutionDataId('display-exec')).setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: 'display-exec',
|
||||
status: 'running',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to displayedExecutionId for filtering and status update', () => {
|
||||
workflowState.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
const ds = useExecutionDataStore(createExecutionDataId('display-exec'));
|
||||
expect(ds.execution?.data?.resultData?.runData?.node1).toHaveLength(1);
|
||||
expect(ds.execution?.data?.resultData?.runData?.node1[0].executionStatus).toBe('success');
|
||||
expect(ds.execution?.status).toBe('canceled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-1');
|
||||
useExecutionDataStore(createExecutionDataId('exec-1')).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: 'exec-1' }),
|
||||
);
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-2');
|
||||
useExecutionDataStore(createExecutionDataId('exec-2')).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: 'exec-2' }),
|
||||
);
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-3');
|
||||
useExecutionDataStore(createExecutionDataId('exec-3')).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: 'exec-3' }),
|
||||
);
|
||||
|
||||
workflowState.resetState();
|
||||
|
||||
expect(useExecutionDataStore(createExecutionDataId('exec-1')).execution).toBeNull();
|
||||
expect(useExecutionDataStore(createExecutionDataId('exec-2')).execution).toBeNull();
|
||||
expect(useExecutionDataStore(createExecutionDataId('exec-3')).execution).toBeNull();
|
||||
});
|
||||
|
||||
it('clears displayedExecutionId so workflowExecutionData reads as null', () => {
|
||||
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).
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
expect(workflowExecutionStateStore.displayedExecutionId).toBe('exec-A');
|
||||
expect(workflowsStore.workflowExecutionData?.id).toBe('exec-A');
|
||||
|
||||
workflowState.resetState();
|
||||
|
||||
const fresh = useWorkflowExecutionStateStore(createWorkflowDocumentId('test-wf'));
|
||||
expect(fresh.displayedExecutionId).toBeUndefined();
|
||||
expect(fresh.activeExecutionId).toBeUndefined();
|
||||
expect(fresh.pendingExecution).toBeNull();
|
||||
expect(workflowsStore.workflowExecutionData).toBeNull();
|
||||
});
|
||||
|
||||
it('disposes the IN_PROGRESS placeholder store along with the pending scaffold', () => {
|
||||
workflowExecutionStateStore.setPendingExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
}),
|
||||
);
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: IN_PROGRESS_EXECUTION_ID }),
|
||||
);
|
||||
|
||||
workflowState.resetState();
|
||||
|
||||
expect(
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).execution,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('is a no-op (other than executingNode/builder reset) when no workflow is loaded', () => {
|
||||
workflowsStore.setWorkflowId('');
|
||||
|
||||
expect(() => workflowState.resetState()).not.toThrow();
|
||||
});
|
||||
|
||||
it('reopening the same workflow id after resetWorkspace order surfaces no stale state', () => {
|
||||
// Stage execution on test-wf
|
||||
workflowExecutionStateStore.setActiveExecutionId('exec-A');
|
||||
useExecutionDataStore(createExecutionDataId('exec-A')).setExecution(
|
||||
createTestWorkflowExecutionResponse({ id: 'exec-A', finished: true, status: 'success' }),
|
||||
);
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
expect(workflowsStore.workflowExecutionData?.id).toBe('exec-A');
|
||||
|
||||
// Mirror resetWorkspace ordering: resetState first (while workflowId is still set),
|
||||
// then resetWorkflow empties the id.
|
||||
workflowState.resetState();
|
||||
workflowsStore.resetWorkflow();
|
||||
expect(workflowsStore.workflowId).toBe('');
|
||||
|
||||
// Reopen the same workflow id.
|
||||
workflowsStore.setWorkflowId('test-wf');
|
||||
|
||||
// Fresh state — no leakage.
|
||||
const fresh = useWorkflowExecutionStateStore(createWorkflowDocumentId('test-wf'));
|
||||
expect(fresh.activeExecutionId).toBeUndefined();
|
||||
expect(fresh.displayedExecutionId).toBeUndefined();
|
||||
expect(fresh.pendingExecution).toBeNull();
|
||||
expect(useExecutionDataStore(createExecutionDataId('exec-A')).execution).toBeNull();
|
||||
expect(workflowsStore.workflowExecutionData).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
import { WorkflowStateKey } from '@/app/constants';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowStateStore } from '@/app/stores/workflowState.store';
|
||||
import {
|
||||
disposeWorkflowExecutionStateStore,
|
||||
useWorkflowExecutionStateStore,
|
||||
} from '@/app/stores/workflowExecutionState.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IExecutionsStopData,
|
||||
} from '@/features/execution/executions/executions.types';
|
||||
import { clearPopupWindowState } from '@/features/execution/executions/executions.utils';
|
||||
import { inject } from 'vue';
|
||||
import { useDocumentTitle } from './useDocumentTitle';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants/placeholders';
|
||||
|
||||
export function useWorkflowState() {
|
||||
const ws = useWorkflowsStore();
|
||||
const workflowStateStore = useWorkflowStateStore();
|
||||
|
||||
////
|
||||
// Workflow editing state
|
||||
////
|
||||
|
||||
function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) {
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(ws.workflowId),
|
||||
);
|
||||
if (workflowResultData === null) {
|
||||
workflowExecutionStateStore.setPendingExecution(null);
|
||||
workflowExecutionStateStore.clearDisplayedExecution();
|
||||
} else if (workflowResultData.id === IN_PROGRESS_EXECUTION_ID) {
|
||||
workflowExecutionStateStore.setPendingExecution(workflowResultData);
|
||||
workflowExecutionStateStore.setActiveExecutionId(null);
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution(
|
||||
workflowResultData,
|
||||
);
|
||||
} else {
|
||||
workflowExecutionStateStore.trackExecutionId(workflowResultData.id);
|
||||
useExecutionDataStore(createExecutionDataId(workflowResultData.id)).setExecution(
|
||||
workflowResultData,
|
||||
);
|
||||
if (typeof workflowExecutionStateStore.activeExecutionId !== 'string') {
|
||||
workflowExecutionStateStore.setPendingExecution(null);
|
||||
workflowExecutionStateStore.setActiveExecutionId(undefined);
|
||||
workflowExecutionStateStore.setDisplayedExecutionId(workflowResultData.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveExecutionId(id: string | null | undefined) {
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId(ws.workflowId)).setActiveExecutionId(
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
////
|
||||
// Execution
|
||||
////
|
||||
|
||||
const documentTitle = useDocumentTitle();
|
||||
|
||||
function markExecutionAsStopped(stopData?: IExecutionsStopData) {
|
||||
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') {
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId(activeExecutionId));
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
} else if (activeExecutionId === null) {
|
||||
// Pending scaffold: filter the IN_PROGRESS placeholder data and
|
||||
// mirror status onto the pendingExecution ref so the UI sees the canceled state.
|
||||
const executionDataStore = useExecutionDataStore(
|
||||
createExecutionDataId(IN_PROGRESS_EXECUTION_ID),
|
||||
);
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
if (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 = workflowExecutionStateStore.displayedExecutionId;
|
||||
if (typeof displayedExecutionId === 'string') {
|
||||
const executionDataStore = useExecutionDataStore(
|
||||
createExecutionDataId(displayedExecutionId),
|
||||
);
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
}
|
||||
}
|
||||
|
||||
clearPopupWindowState();
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
const wid = ws.workflowId;
|
||||
if (!wid) {
|
||||
workflowStateStore.executingNode.executingNode.length = 0;
|
||||
useBuilderStore().resetManualExecutionStats();
|
||||
return;
|
||||
}
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(wid),
|
||||
);
|
||||
// Disposes every tracked executionData store + IN_PROGRESS placeholder, then clears all
|
||||
// session-level fields.
|
||||
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(workflowExecutionStateStore);
|
||||
|
||||
workflowStateStore.executingNode.executingNode.length = 0;
|
||||
useBuilderStore().resetManualExecutionStats();
|
||||
}
|
||||
|
||||
return {
|
||||
// Workflow editing state
|
||||
resetState,
|
||||
setWorkflowExecutionData,
|
||||
setActiveExecutionId,
|
||||
|
||||
// Execution
|
||||
markExecutionAsStopped,
|
||||
|
||||
// reexport
|
||||
executingNode: workflowStateStore.executingNode,
|
||||
};
|
||||
}
|
||||
|
||||
export type WorkflowState = ReturnType<typeof useWorkflowState>;
|
||||
|
||||
export function injectWorkflowState() {
|
||||
return inject(
|
||||
WorkflowStateKey,
|
||||
() => {
|
||||
// While we're migrating we're happy to fall back onto a separate instance since
|
||||
// all data is still stored in the workflowsStore
|
||||
// Once we're ready to move the actual refs to `useWorkflowState` we should error here
|
||||
// to track down remaining usages that would break
|
||||
// console.error('Attempted to inject workflowState outside of NodeView tree');
|
||||
return useWorkflowState();
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
|
@ -58,14 +58,6 @@ vi.mock('@/app/stores/workflowDocument.store', () => ({
|
|||
injectWorkflowDocumentStore: vi.fn().mockReturnValue({ value: mockDocumentStore }),
|
||||
}));
|
||||
|
||||
// Mock useWorkflowState - using hoisted for proper initialization
|
||||
const mockWorkflowState = vi.hoisted(() => ({
|
||||
touchParametersLastUpdatedAt: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(() => mockWorkflowState),
|
||||
}));
|
||||
|
||||
// Mock useCanvasOperations - using hoisted for proper initialization
|
||||
const mockCanvasOperations = vi.hoisted(() => ({
|
||||
deleteNode: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type {
|
|||
import type { ComputedRef, InjectionKey, Ref, ShallowRef } from 'vue';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
import type { TelemetryContext } from '@/app/types/telemetry';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { CanvasRenderData } from '@/features/workflows/canvas/canvas.utils';
|
||||
|
|
@ -22,7 +21,6 @@ export const ExpressionLocalResolveContextSymbol: InjectionKey<
|
|||
ComputedRef<ExpressionLocalResolveContext | undefined>
|
||||
> = Symbol('ExpressionLocalResolveContext');
|
||||
export const TelemetryContextSymbol: InjectionKey<TelemetryContext> = Symbol('TelemetryContext');
|
||||
export const WorkflowStateKey: InjectionKey<WorkflowState> = Symbol('WorkflowState');
|
||||
export const WorkflowDocumentStoreKey: InjectionKey<ShallowRef<WorkflowDocumentStore | null>> =
|
||||
Symbol('WorkflowDocumentStore');
|
||||
export const ExecutionDataStoreKey: InjectionKey<
|
||||
|
|
|
|||
|
|
@ -30,17 +30,6 @@ vi.mock('vue-router', async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as object;
|
||||
return {
|
||||
...actual,
|
||||
useWorkflowState: vi.fn(() => ({
|
||||
setActiveExecutionId: vi.fn(),
|
||||
resetState: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowInitialization', () => ({
|
||||
useWorkflowInitialization: vi.fn(() => ({
|
||||
isLoading: ref(false),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, provide, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import BaseLayout from './BaseLayout.vue';
|
||||
import DemoFooter from '@/features/execution/logs/components/DemoFooter.vue';
|
||||
import { WorkflowStateKey } from '@/app/constants/injectionKeys';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowId } from '@/app/composables/useWorkflowId';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowInitialization } from '@/app/composables/useWorkflowInitialization';
|
||||
import { usePostMessageHandler } from '@/app/composables/usePostMessageHandler';
|
||||
import { useReportWorkflowFailuresToParent } from '@/app/composables/useReportWorkflowFailuresToParent';
|
||||
|
|
@ -24,18 +25,16 @@ if (window !== window.parent) {
|
|||
useRootStore().setPushRef(randomString(10).toLowerCase());
|
||||
}
|
||||
|
||||
const workflowState = useWorkflowState();
|
||||
provide(WorkflowStateKey, workflowState);
|
||||
const workflowId = useWorkflowId();
|
||||
|
||||
const {
|
||||
initializeData,
|
||||
initializeWorkflow,
|
||||
currentWorkflowDocumentStore,
|
||||
cleanup: cleanupInitialization,
|
||||
} = useWorkflowInitialization(workflowState);
|
||||
} = useWorkflowInitialization();
|
||||
|
||||
const { setup: setupPostMessages, cleanup: cleanupPostMessages } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore,
|
||||
});
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ useReportWorkflowFailuresToParent();
|
|||
// from the parent) are processed for node highlighting, execution state, etc.
|
||||
// When canExecute is enabled, the iframe also establishes its own WebSocket
|
||||
// connection for user-triggered executions (pushConnect below).
|
||||
const pushConnection = usePushConnection({ router: useRouter(), workflowState });
|
||||
const pushConnection = usePushConnection({ router: useRouter() });
|
||||
const pushConnectionStore = usePushConnectionStore();
|
||||
|
||||
// When canExecute is disabled (read-only preview), set activeExecutionId to null
|
||||
|
|
@ -54,7 +53,9 @@ const pushConnectionStore = usePushConnectionStore();
|
|||
// button is not disabled — the normal execution flow will set it to null when
|
||||
// the user actually starts an execution.
|
||||
if (!canExecute.value) {
|
||||
workflowState.setActiveExecutionId(null);
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowId.value)).setActiveExecutionId(
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
|
|||
|
|
@ -33,16 +33,6 @@ vi.mock('@/features/ai/assistant/assistant.store', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', () => {
|
||||
const mockState = () => ({
|
||||
resetState: vi.fn(),
|
||||
});
|
||||
return {
|
||||
useWorkflowState: vi.fn(mockState),
|
||||
injectWorkflowState: vi.fn(mockState),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowInitialization', () => ({
|
||||
useWorkflowInitialization: vi.fn(() => ({
|
||||
isLoading: ref(false),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { provide, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import BaseLayout from './BaseLayout.vue';
|
||||
import { useLayoutProps } from '@/app/composables/useLayoutProps';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowInitialization } from '@/app/composables/useWorkflowInitialization';
|
||||
import { usePostMessageHandler } from '@/app/composables/usePostMessageHandler';
|
||||
import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
|
||||
|
|
@ -14,7 +13,6 @@ import AppHeader from '@/app/components/app/AppHeader.vue';
|
|||
import AppSidebar from '@/app/components/app/AppSidebar.vue';
|
||||
import LogsPanel from '@/features/execution/logs/components/LogsPanel.vue';
|
||||
import LoadingView from '@/app/views/LoadingView.vue';
|
||||
import { WorkflowStateKey } from '@/app/constants/injectionKeys';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
|
||||
const { layoutProps } = useLayoutProps();
|
||||
|
|
@ -24,9 +22,6 @@ const pushConnectionStore = usePushConnectionStore();
|
|||
const settingsStore = useSettingsStore();
|
||||
const isCanvasOnly = settingsStore.isCanvasOnly;
|
||||
|
||||
const workflowState = useWorkflowState();
|
||||
provide(WorkflowStateKey, workflowState);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
workflowId,
|
||||
|
|
@ -36,10 +31,9 @@ const {
|
|||
initializeWorkflow,
|
||||
handleDebugModeRoute,
|
||||
cleanup,
|
||||
} = useWorkflowInitialization(workflowState);
|
||||
} = useWorkflowInitialization();
|
||||
|
||||
const { setup: setupPostMessages, cleanup: cleanupPostMessages } = usePostMessageHandler({
|
||||
workflowState,
|
||||
currentWorkflowDocumentStore,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import {
|
|||
} from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useExecutionDataStore, createExecutionDataId } from '@/app/stores/executionData.store';
|
||||
import { createTestWorkflowExecutionResponse } from '@/__tests__/mocks';
|
||||
import { createTestTaskData, createTestWorkflowExecutionResponse } from '@/__tests__/mocks';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type { ExecutionSummary } from 'n8n-workflow';
|
||||
import { createRunExecutionData, type ExecutionSummary } from 'n8n-workflow';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants/placeholders';
|
||||
|
||||
function makeExecution(overrides: Partial<IExecutionResponse> = {}): IExecutionResponse {
|
||||
|
|
@ -131,6 +131,227 @@ describe('workflowExecutionState.store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setWorkflowExecutionData', () => {
|
||||
it('clears pending + displayed execution when given null', () => {
|
||||
const store = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-1'));
|
||||
store.setActiveExecutionId('exec-1');
|
||||
store.setPendingExecution(makeExecution({ id: 'pending' }));
|
||||
|
||||
store.setWorkflowExecutionData(null);
|
||||
|
||||
expect(store.pendingExecution).toBeNull();
|
||||
expect(store.displayedExecutionId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('stages an in-progress execution as the pending scaffold', () => {
|
||||
const store = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-1'));
|
||||
const execution = makeExecution({ id: IN_PROGRESS_EXECUTION_ID });
|
||||
|
||||
store.setWorkflowExecutionData(execution);
|
||||
|
||||
expect(store.activeExecutionId).toBeNull();
|
||||
expect(store.pendingExecution?.id).toBe(IN_PROGRESS_EXECUTION_ID);
|
||||
const executionDataStore = useExecutionDataStore(
|
||||
createExecutionDataId(IN_PROGRESS_EXECUTION_ID),
|
||||
);
|
||||
expect(executionDataStore.execution?.id).toBe(IN_PROGRESS_EXECUTION_ID);
|
||||
});
|
||||
|
||||
it('tracks a finished execution as displayed when none is active', () => {
|
||||
const store = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-1'));
|
||||
const execution = makeExecution({ id: 'exec-9', status: 'success', finished: true });
|
||||
|
||||
store.setWorkflowExecutionData(execution);
|
||||
|
||||
expect(store.displayedExecutionId).toBe('exec-9');
|
||||
expect(store.activeExecutionId).toBeUndefined();
|
||||
expect(store.pendingExecution).toBeNull();
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-9'));
|
||||
expect(executionDataStore.execution?.id).toBe('exec-9');
|
||||
});
|
||||
|
||||
it('leaves an active execution id untouched when finished data arrives', () => {
|
||||
const store = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-1'));
|
||||
store.setActiveExecutionId('exec-active');
|
||||
|
||||
store.setWorkflowExecutionData(makeExecution({ id: 'exec-9', finished: true }));
|
||||
|
||||
expect(store.activeExecutionId).toBe('exec-active');
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-9'));
|
||||
expect(executionDataStore.execution?.id).toBe('exec-9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markExecutionAsStopped', () => {
|
||||
const documentId = createWorkflowDocumentId('test-wf');
|
||||
let store: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
let executionDataStore: ReturnType<typeof useExecutionDataStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
store = useWorkflowExecutionStateStore(documentId);
|
||||
store.setActiveExecutionId('test-exec-id');
|
||||
|
||||
executionDataStore = useExecutionDataStore(createExecutionDataId('test-exec-id'));
|
||||
executionDataStore.setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: 'test-exec-id',
|
||||
status: 'running',
|
||||
startedAt: new Date('2023-01-01T09:00:00Z'),
|
||||
stoppedAt: undefined,
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
createTestTaskData({ executionStatus: 'running' }),
|
||||
],
|
||||
node2: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'waiting' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove non successful node runs', () => {
|
||||
store.markExecutionAsStopped();
|
||||
|
||||
const runData = executionDataStore.execution?.data?.resultData?.runData;
|
||||
expect(runData?.node1).toHaveLength(1);
|
||||
expect(runData?.node1[0].executionStatus).toBe('success');
|
||||
expect(runData?.node2).toHaveLength(1);
|
||||
expect(runData?.node2[0].executionStatus).toBe('success');
|
||||
});
|
||||
|
||||
it('should update execution status, startedAt and stoppedAt when data is provided', () => {
|
||||
store.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
expect(executionDataStore.execution?.status).toBe('canceled');
|
||||
expect(executionDataStore.execution?.startedAt).toEqual(new Date('2023-01-01T10:00:00Z'));
|
||||
expect(executionDataStore.execution?.stoppedAt).toEqual(new Date('2023-01-01T10:05:00Z'));
|
||||
});
|
||||
|
||||
it('should not update execution data when stopData is not provided', () => {
|
||||
store.markExecutionAsStopped();
|
||||
|
||||
expect(executionDataStore.execution?.status).toBe('running');
|
||||
expect(executionDataStore.execution?.startedAt).toEqual(new Date('2023-01-01T09:00:00Z'));
|
||||
expect(executionDataStore.execution?.stoppedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('when activeExecutionId is null (pending scaffold)', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to pending state instead of the string-id default from outer beforeEach.
|
||||
store.setActiveExecutionId(undefined);
|
||||
store.setPendingExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
}),
|
||||
);
|
||||
// Re-set since promotePendingExecution would have moved it; emulate raw scaffold state.
|
||||
store.setActiveExecutionId(null);
|
||||
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('filters non-success runs in the IN_PROGRESS placeholder store', () => {
|
||||
store.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
const placeholder = useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID));
|
||||
expect(placeholder.execution?.data?.resultData?.runData?.node1).toHaveLength(1);
|
||||
expect(placeholder.execution?.data?.resultData?.runData?.node1[0].executionStatus).toBe(
|
||||
'success',
|
||||
);
|
||||
expect(placeholder.execution?.status).toBe('canceled');
|
||||
});
|
||||
|
||||
it('mirrors stopData onto the pendingExecution ref', () => {
|
||||
store.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
expect(store.pendingExecution?.status).toBe('canceled');
|
||||
expect(store.pendingExecution?.startedAt).toEqual(new Date('2023-01-01T10:00:00Z'));
|
||||
expect(store.pendingExecution?.stoppedAt).toEqual(new Date('2023-01-01T10:05:00Z'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when activeExecutionId is undefined and displayedExecutionId is set', () => {
|
||||
beforeEach(() => {
|
||||
// Simulate post-stop-race: active was just cleared, but displayed still points
|
||||
// at the freshly-fetched finished execution.
|
||||
store.setActiveExecutionId('display-exec');
|
||||
store.setActiveExecutionId(undefined);
|
||||
expect(store.activeExecutionId).toBeUndefined();
|
||||
expect(store.displayedExecutionId).toBe('display-exec');
|
||||
|
||||
useExecutionDataStore(createExecutionDataId('display-exec')).setExecution(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: 'display-exec',
|
||||
status: 'running',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
node1: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'error' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to displayedExecutionId for filtering and status update', () => {
|
||||
store.markExecutionAsStopped({
|
||||
status: 'canceled',
|
||||
startedAt: new Date('2023-01-01T10:00:00Z'),
|
||||
stoppedAt: new Date('2023-01-01T10:05:00Z'),
|
||||
mode: 'manual',
|
||||
});
|
||||
|
||||
const ds = useExecutionDataStore(createExecutionDataId('display-exec'));
|
||||
expect(ds.execution?.data?.resultData?.runData?.node1).toHaveLength(1);
|
||||
expect(ds.execution?.data?.resultData?.runData?.node1[0].executionStatus).toBe('success');
|
||||
expect(ds.execution?.status).toBe('canceled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('activeExecution routing', () => {
|
||||
it('returns pendingExecution when activeExecutionId === null', () => {
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
|
|
@ -1057,6 +1278,7 @@ describe('workflowExecutionState.store', () => {
|
|||
workflowExecutionStateStore.setSelectedTriggerNodeName('Trigger');
|
||||
workflowExecutionStateStore.setCurrentWorkflowExecutions([makeExecutionSummary({ id: '1' })]);
|
||||
workflowExecutionStateStore.setLastSuccessfulExecutionId('last-1');
|
||||
workflowExecutionStateStore.executingNode.addExecutingNode('Node');
|
||||
|
||||
workflowExecutionStateStore.resetExecutionState();
|
||||
|
||||
|
|
@ -1071,6 +1293,8 @@ describe('workflowExecutionState.store', () => {
|
|||
expect(workflowExecutionStateStore.selectedTriggerNodeName).toBeUndefined();
|
||||
expect(workflowExecutionStateStore.currentWorkflowExecutions).toEqual([]);
|
||||
expect(workflowExecutionStateStore.lastSuccessfulExecutionId).toBeNull();
|
||||
expect(workflowExecutionStateStore.executingNode.executingNode).toEqual([]);
|
||||
expect(workflowExecutionStateStore.executingNode.lastAddedExecutingNode).toBeNull();
|
||||
});
|
||||
|
||||
it('disposes per-execution data stores for every tracked id', () => {
|
||||
|
|
@ -1119,6 +1343,30 @@ describe('workflowExecutionState.store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('executingNode', () => {
|
||||
it('tracks the executing-node queue', () => {
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId('wf-1'),
|
||||
);
|
||||
|
||||
workflowExecutionStateStore.executingNode.addExecutingNode('Node A');
|
||||
|
||||
expect(workflowExecutionStateStore.executingNode.isNodeExecuting('Node A')).toBe(true);
|
||||
expect(workflowExecutionStateStore.executingNode.lastAddedExecutingNode).toBe('Node A');
|
||||
});
|
||||
|
||||
it('isolates the executing-node queue per document id', () => {
|
||||
const a = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-a'));
|
||||
const b = useWorkflowExecutionStateStore(createWorkflowDocumentId('wf-b'));
|
||||
|
||||
a.executingNode.addExecutingNode('Node A');
|
||||
|
||||
expect(a.executingNode.isNodeExecuting('Node A')).toBe(true);
|
||||
expect(b.executingNode.isNodeExecuting('Node A')).toBe(false);
|
||||
expect(b.executingNode.executingNode).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackExecutionId', () => {
|
||||
it('tracks ids written via setActiveExecutionId across rolling runs', () => {
|
||||
const workflowExecutionStateStore = useWorkflowExecutionStateStore(
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import { computed, inject, readonly, ref, type ComputedRef } from 'vue';
|
|||
import { createEventHook } from '@vueuse/core';
|
||||
import type { ExecutionSummary, IRunExecutionData } from 'n8n-workflow';
|
||||
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IExecutionsStopData,
|
||||
} from '@/features/execution/executions/executions.types';
|
||||
import { WorkflowExecutionStateStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants/placeholders';
|
||||
import { useExecutingNode } from '@/app/composables/useExecutingNode';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import {
|
||||
createExecutionDataId,
|
||||
|
|
@ -14,6 +18,8 @@ import {
|
|||
useExecutionDataStore,
|
||||
} from './executionData.store';
|
||||
import { useWorkflowDocumentStore, type WorkflowDocumentId } from './workflowDocument.store';
|
||||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import { clearPopupWindowState } from '@/features/execution/executions/executions.utils';
|
||||
import { CHANGE_ACTION } from './workflowDocument/types';
|
||||
import type { ChangeAction, ChangeEvent } from './workflowDocument/types';
|
||||
|
||||
|
|
@ -97,6 +103,14 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
*/
|
||||
const trackedExecutionIds = ref<Set<string>>(new Set());
|
||||
|
||||
/**
|
||||
* Queue of currently-executing node names driving per-node loading
|
||||
* spinners. Owned by the per-document store so spinner state stays
|
||||
* isolated per workflow document. Read purely via Vue reactivity; it is
|
||||
* intentionally not wired into the change-event mechanism below.
|
||||
*/
|
||||
const executingNode = useExecutingNode();
|
||||
|
||||
const onWorkflowExecutionStateChange = createEventHook<WorkflowExecutionStateChangeEvent>();
|
||||
|
||||
function fireChange(action: ChangeAction, field: WorkflowExecutionStateField) {
|
||||
|
|
@ -342,6 +356,34 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
fireChange(CHANGE_ACTION.UPDATE, 'pendingExecution');
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a fetched/started execution result to this document's session state:
|
||||
* clears it when null, stages it as the pending scaffold while in progress, or
|
||||
* tracks it as a displayed execution once it has a backend id.
|
||||
*/
|
||||
function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) {
|
||||
if (workflowResultData === null) {
|
||||
setPendingExecution(null);
|
||||
clearDisplayedExecution();
|
||||
} else if (workflowResultData.id === IN_PROGRESS_EXECUTION_ID) {
|
||||
setPendingExecution(workflowResultData);
|
||||
setActiveExecutionId(null);
|
||||
useExecutionDataStore(createExecutionDataId(IN_PROGRESS_EXECUTION_ID)).setExecution(
|
||||
workflowResultData,
|
||||
);
|
||||
} else {
|
||||
trackExecutionId(workflowResultData.id);
|
||||
useExecutionDataStore(createExecutionDataId(workflowResultData.id)).setExecution(
|
||||
workflowResultData,
|
||||
);
|
||||
if (typeof activeExecutionId.value !== 'string') {
|
||||
setPendingExecution(null);
|
||||
setActiveExecutionId(undefined);
|
||||
setDisplayedExecutionId(workflowResultData.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveNodeExecutionData(nodeName: string) {
|
||||
if (typeof activeExecutionId.value !== 'string') return;
|
||||
useExecutionDataStore(createExecutionDataId(activeExecutionId.value)).clearNodeExecutionData(
|
||||
|
|
@ -587,9 +629,55 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
selectedTriggerNodeName.value = undefined;
|
||||
currentWorkflowExecutions.value = [];
|
||||
lastSuccessfulExecutionId.value = null;
|
||||
executingNode.clearNodeExecutionQueue();
|
||||
fireChange(CHANGE_ACTION.DELETE, 'state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this document's execution session after a stop: clears the active
|
||||
* execution id / executing-node queue / webhook-wait, restores the IDLE
|
||||
* document title, and marks the relevant executionData store as stopped
|
||||
* (active id → IN_PROGRESS scaffold → displayed-id fallback for the
|
||||
* stop-race-with-finished case).
|
||||
*/
|
||||
function markExecutionAsStopped(stopData?: IExecutionsStopData) {
|
||||
const activeId = activeExecutionId.value;
|
||||
|
||||
setActiveExecutionId(undefined);
|
||||
executingNode.clearNodeExecutionQueue();
|
||||
setExecutionWaitingForWebhook(false);
|
||||
|
||||
useDocumentTitle().setDocumentTitle(useWorkflowDocumentStore(documentId).name, 'IDLE');
|
||||
|
||||
if (typeof activeId === 'string') {
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId(activeId));
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
} else if (activeId === null) {
|
||||
// Pending scaffold: filter the IN_PROGRESS placeholder data and
|
||||
// mirror status onto the pendingExecution ref so the UI sees the canceled state.
|
||||
const executionDataStore = useExecutionDataStore(
|
||||
createExecutionDataId(IN_PROGRESS_EXECUTION_ID),
|
||||
);
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
if (stopData) {
|
||||
applyStopDataToPendingExecution(stopData);
|
||||
}
|
||||
} else {
|
||||
// activeExecutionId === undefined: fall back to displayedExecutionId for the
|
||||
// stop-race-with-finished case where active was just cleared.
|
||||
const displayedId = displayedExecutionId.value;
|
||||
if (typeof displayedId === 'string') {
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId(displayedId));
|
||||
executionDataStore.clearExecutionStartedData();
|
||||
executionDataStore.markAsStopped(stopData);
|
||||
}
|
||||
}
|
||||
|
||||
clearPopupWindowState();
|
||||
}
|
||||
|
||||
return {
|
||||
documentId,
|
||||
workflowId,
|
||||
|
|
@ -605,6 +693,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
selectedTriggerNodeName: readonly(selectedTriggerNodeName),
|
||||
currentWorkflowExecutions: readonly(currentWorkflowExecutions),
|
||||
lastSuccessfulExecutionId: readonly(lastSuccessfulExecutionId),
|
||||
executingNode,
|
||||
activeExecution,
|
||||
activeExecutionRunData,
|
||||
activeExecutionExecutedNode,
|
||||
|
|
@ -621,6 +710,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
// Write API
|
||||
trackExecutionId,
|
||||
setActiveExecutionId,
|
||||
setWorkflowExecutionData,
|
||||
setDisplayedExecutionId,
|
||||
setPendingExecution,
|
||||
setPendingExecutionRunData,
|
||||
|
|
@ -648,6 +738,7 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
|||
addActiveNodeExecutionStartedData,
|
||||
renameActiveExecutionNode,
|
||||
resetExecutionState,
|
||||
markExecutionAsStopped,
|
||||
// Events
|
||||
onWorkflowExecutionStateChange: onWorkflowExecutionStateChange.on,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { useExecutingNode } from '@/app/composables/useExecutingNode';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
// This store acts as a temporary home for per-workflow state moved out of workflows.store.ts
|
||||
// intended for the useWorkflowState composable until we inject a single instance
|
||||
// of it for any applicable component
|
||||
export const useWorkflowStateStore = defineStore(STORES.WORKFLOW_STATE, () => {
|
||||
const executingNode = useExecutingNode();
|
||||
|
||||
return {
|
||||
executingNode,
|
||||
};
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ vi.mock('@/features/execution/executions/executions.store', () => ({
|
|||
|
||||
vi.mock('@/app/stores/workflows.store', () => ({
|
||||
useWorkflowsStore: () => ({
|
||||
workflowId: 'wf-1',
|
||||
workflowExecutionData: null,
|
||||
getNodeTypes: () => ({
|
||||
getByName: () => undefined,
|
||||
|
|
@ -21,8 +22,8 @@ vi.mock('@/app/stores/workflows.store', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
useWorkflowState: () => ({
|
||||
vi.mock('@/app/stores/workflowExecutionState.store', () => ({
|
||||
useWorkflowExecutionStateStore: () => ({
|
||||
setWorkflowExecutionData: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { ChatSymbol } from '@n8n/chat/constants';
|
|||
import type { Chat } from '@n8n/chat/types';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import RunData from '@/features/ndv/runData/components/RunData.vue';
|
||||
|
|
@ -50,7 +51,6 @@ const props = withDefaults(
|
|||
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = useWorkflowState();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
|
|
@ -221,12 +221,16 @@ onMounted(async () => {
|
|||
// hook already restored the previous execution data — installing the synth
|
||||
// payload now would clobber the real workflow's state.
|
||||
if (unmounted) return;
|
||||
workflowState.setWorkflowExecutionData(synthExecution.value);
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData(synthExecution.value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unmounted = true;
|
||||
workflowState.setWorkflowExecutionData(previousWorkflowExecutionData);
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData(previousWorkflowExecutionData);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import type { Chat } from '@n8n/chat/types';
|
|||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { useExecutionsStore } from '@/features/execution/executions/executions.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import LogsOverviewRow from '@/features/execution/logs/components/LogsOverviewRow.vue';
|
||||
|
|
@ -30,7 +31,6 @@ const props = defineProps<{
|
|||
const i18n = useI18n();
|
||||
const executionsStore = useExecutionsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = useWorkflowState();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
|
|
@ -176,7 +176,9 @@ onMounted(async () => {
|
|||
// pairedItemMappings, and various NodeErrorView code paths read from the
|
||||
// store rather than the prop, so the prop alone isn't enough to make the
|
||||
// table/JSON views render correctly for non-trivial nodes.
|
||||
workflowState.setWorkflowExecutionData(result);
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData(result);
|
||||
// Default-select the first entry (the trigger) so the user sees data immediately.
|
||||
const first = flatEntries.value[0];
|
||||
if (first) selected.value = first;
|
||||
|
|
@ -192,7 +194,9 @@ onMounted(async () => {
|
|||
onBeforeUnmount(() => {
|
||||
unmounted = true;
|
||||
// Restore whatever execution data was in the store before we hijacked it.
|
||||
workflowState.setWorkflowExecutionData(previousWorkflowExecutionData);
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData(previousWorkflowExecutionData);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,6 @@ import { DEFAULT_POSTHOG_SETTINGS } from '@/app/stores/posthog.store.test';
|
|||
import { nextTick, reactive } from 'vue';
|
||||
import * as chatAPI from '@/features/ai/assistant/assistant.api';
|
||||
import * as telemetryModule from '@/app/composables/useTelemetry';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import type { Telemetry } from '@/app/plugins/telemetry';
|
||||
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
||||
import type { ChatRequest } from '@/features/ai/assistant/assistant.types';
|
||||
|
|
@ -69,15 +64,6 @@ vi.mock('@/app/composables/useToast', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
// Mock to inject workflowState
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useWorkflowSaving
|
||||
const saveCurrentWorkflowMock = vi.fn().mockResolvedValue(true);
|
||||
vi.mock('@/app/composables/useWorkflowSaving', () => ({
|
||||
|
|
@ -148,8 +134,6 @@ vi.mock('vue-router', () => ({
|
|||
RouterLink: vi.fn(),
|
||||
}));
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
describe('AI Builder store', () => {
|
||||
beforeEach(() => {
|
||||
mockDocumentState = undefined;
|
||||
|
|
@ -182,9 +166,6 @@ describe('AI Builder store', () => {
|
|||
workflowDocumentStore.setConnections({});
|
||||
workflowsStore.setWorkflowExecutionData(null);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
getNodeTypeSpy = vi.fn();
|
||||
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(getNodeTypeSpy);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
|
|
@ -66,17 +66,6 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
// Mock useWorkflowState
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(() => ({
|
||||
isWorkflowRunning: ref(false),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useWorkflowSaving
|
||||
vi.mock('@/app/composables/useWorkflowSaving', () => ({
|
||||
useWorkflowSaving: () => ({
|
||||
|
|
|
|||
|
|
@ -65,13 +65,6 @@ vi.mock('@/app/stores/ui.store', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const mockUpdateNodeProperties = vi.fn();
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: () => ({
|
||||
updateNodeProperties: mockUpdateNodeProperties,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useNodeHelpers', () => ({
|
||||
useNodeHelpers: () => ({
|
||||
updateNodesParameterIssues: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
|
@ -9,10 +9,6 @@ import { useCredentialsStore } from '../../credentials.store';
|
|||
import { mockedStore } from '@/__tests__/utils';
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(),
|
||||
}));
|
||||
|
||||
const googleSheetsOAuth2Api: ICredentialType = {
|
||||
name: 'googleSheetsOAuth2Api',
|
||||
extends: ['googleOAuth2Api'],
|
||||
|
|
|
|||
|
|
@ -2,16 +2,12 @@ import { createTestingPinia } from '@pinia/testing';
|
|||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { useExecutionDebugging } from './useExecutionDebugging';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { IExecutionResponse } from '../executions.types';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow';
|
||||
|
||||
vi.mock('@/app/composables/useToast', () => {
|
||||
|
|
@ -23,16 +19,9 @@ vi.mock('@/app/composables/useToast', () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
|
||||
mockWorkflowDocumentStore: {
|
||||
documentId: 'test-id@latest',
|
||||
allNodes: [] as INodeUi[],
|
||||
workflowTriggerNodes: [] as INodeUi[],
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
|
|
@ -51,9 +40,9 @@ vi.mock('@/app/stores/workflowDocument.store', () => ({
|
|||
injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }),
|
||||
}));
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
let executionDebugging: ReturnType<typeof useExecutionDebugging>;
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
let executionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
|
||||
describe('useExecutionDebugging()', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -68,8 +57,9 @@ describe('useExecutionDebugging()', () => {
|
|||
|
||||
toast = useToast();
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
// Production resolves the execution-state store by the injected document
|
||||
// store's `documentId` ('test-id@latest' on the mock above).
|
||||
executionStateStore = useWorkflowExecutionStateStore('test-id@latest');
|
||||
|
||||
executionDebugging = useExecutionDebugging();
|
||||
});
|
||||
|
|
@ -192,7 +182,7 @@ describe('useExecutionDebugging()', () => {
|
|||
mockWorkflowDocumentStore.allNodes = [{ name: 'testNode2' }] as INodeUi[];
|
||||
workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
|
||||
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
await executionDebugging.applyExecutionData('1');
|
||||
|
||||
|
|
@ -220,7 +210,7 @@ describe('useExecutionDebugging()', () => {
|
|||
mockWorkflowDocumentStore.allNodes = [{ name: 'testNode' }] as INodeUi[];
|
||||
workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
|
||||
|
||||
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');
|
||||
const setWorkflowExecutionData = vi.spyOn(executionStateStore, 'setWorkflowExecutionData');
|
||||
|
||||
await executionDebugging.applyExecutionData('1');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useRouter } from 'vue-router';
|
|||
import { useI18n } from '@n8n/i18n';
|
||||
import { useMessage } from '@/app/composables/useMessage';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/app/constants';
|
||||
import { DEBUG_PAYWALL_MODAL_KEY } from '../executions.constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
|
@ -22,12 +21,7 @@ import { sanitizeHtml } from '@/app/utils/htmlUtils';
|
|||
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
|
||||
import { isTrimmedNodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* @param providedWorkflowState - Optional workflow state to use instead of injecting.
|
||||
* This is needed when called from the same component that provides WorkflowStateKey
|
||||
* (e.g., WorkflowLayout), since Vue's provide/inject works parent-to-child only.
|
||||
*/
|
||||
export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) => {
|
||||
export const useExecutionDebugging = () => {
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -36,7 +30,6 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) =>
|
|||
const toast = useToast();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowState = providedWorkflowState ?? injectWorkflowState();
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const { markStateDirty } = uiStore;
|
||||
|
|
@ -106,7 +99,9 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) =>
|
|||
|
||||
// Set execution data
|
||||
workflowDocumentStore.value.resetAllNodesIssues();
|
||||
workflowState.setWorkflowExecutionData(execution);
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId).setWorkflowExecutionData(
|
||||
execution,
|
||||
);
|
||||
|
||||
// Pin data of all nodes which do not have a parent node
|
||||
const pinnableNodes = workflowNodes.filter(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {
|
|||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import { computed, h, nextTick, ref, shallowRef } from 'vue';
|
||||
import {
|
||||
aiAgentNode,
|
||||
|
|
@ -21,7 +23,7 @@ import {
|
|||
nodeTypes,
|
||||
} from '../__test__/data';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { IN_PROGRESS_EXECUTION_ID, WorkflowStateKey } from '@/app/constants';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
|
|
@ -36,7 +38,6 @@ import { userEvent } from '@testing-library/user-event';
|
|||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import * as useChatMessaging from '@/features/execution/logs/composables/useChatMessaging';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
|
||||
vi.mock('@/app/composables/useToast', () => {
|
||||
|
|
@ -81,7 +82,6 @@ describe('LogsPanel', () => {
|
|||
let logsStore: ReturnType<typeof mockedStore<typeof useLogsStore>>;
|
||||
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
let aiChatExecutionResponse: typeof aiChatExecutionResponseTemplate;
|
||||
|
||||
|
|
@ -92,6 +92,15 @@ describe('LogsPanel', () => {
|
|||
ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflow.id));
|
||||
}
|
||||
|
||||
// Production stages execution data on the per-document execution-state store.
|
||||
// The component resolves it from the injected document store, which is keyed by
|
||||
// `workflowsStore.workflowId` — so resolve the same store here for test setup.
|
||||
function setExecutionData(execution: IExecutionResponse | null) {
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData(execution);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const wfId = workflowsStore.workflowId;
|
||||
const wrapper = renderComponent(LogsPanel, {
|
||||
|
|
@ -99,7 +108,6 @@ describe('LogsPanel', () => {
|
|||
provide: {
|
||||
[ChatSymbol as symbol]: {},
|
||||
[ChatOptionsSymbol as symbol]: {},
|
||||
[WorkflowStateKey as symbol]: workflowState,
|
||||
[WorkflowIdKey as unknown as string]: computed(() => wfId),
|
||||
[WorkflowDocumentStoreKey as symbol]: shallowRef(
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(wfId)),
|
||||
|
|
@ -128,8 +136,7 @@ describe('LogsPanel', () => {
|
|||
setActivePinia(pinia);
|
||||
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowState = useWorkflowState();
|
||||
workflowState.setWorkflowExecutionData(null);
|
||||
setExecutionData(null);
|
||||
|
||||
logsStore = mockedStore(useLogsStore);
|
||||
logsStore.toggleOpen(false);
|
||||
|
|
@ -190,7 +197,7 @@ describe('LogsPanel', () => {
|
|||
it('should render only output panel of selected node by default', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiManualWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiManualExecutionResponse);
|
||||
setExecutionData(aiManualExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -204,7 +211,7 @@ describe('LogsPanel', () => {
|
|||
it('should render both input and output panel of selected node by default if it is sub node', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -247,7 +254,7 @@ describe('LogsPanel', () => {
|
|||
|
||||
it('should open log details panel when a log entry is clicked in the logs overview panel', async () => {
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -264,7 +271,7 @@ describe('LogsPanel', () => {
|
|||
|
||||
it("should show the button to toggle panel in the header of log details panel when it's opened", async () => {
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -329,7 +336,7 @@ describe('LogsPanel', () => {
|
|||
it('should reflect changes to execution data in workflow store if execution is in progress', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData({
|
||||
setExecutionData({
|
||||
...aiChatExecutionResponse,
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
|
|
@ -388,7 +395,7 @@ describe('LogsPanel', () => {
|
|||
expect(await lastTreeItem.findByText('Success')).toBeInTheDocument();
|
||||
expect(lastTreeItem.getByText('in 33ms')).toBeInTheDocument();
|
||||
|
||||
workflowState.setWorkflowExecutionData({
|
||||
setExecutionData({
|
||||
...workflowsStore.workflowExecutionData!,
|
||||
id: '1234',
|
||||
status: 'success',
|
||||
|
|
@ -409,7 +416,7 @@ describe('LogsPanel', () => {
|
|||
const workflow = deepCopy(aiChatWorkflow);
|
||||
setWorkflow(workflow);
|
||||
logsStore.toggleOpen(true);
|
||||
workflowState.setWorkflowExecutionData({
|
||||
setExecutionData({
|
||||
...aiChatExecutionResponse,
|
||||
id: '2345',
|
||||
status: 'success',
|
||||
|
|
@ -434,7 +441,7 @@ describe('LogsPanel', () => {
|
|||
it('should open NDV if the button is clicked', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
const aiAgentRow = (await rendered.findAllByRole('treeitem'))[0];
|
||||
|
|
@ -453,7 +460,7 @@ describe('LogsPanel', () => {
|
|||
it('should toggle subtree when chevron icon button is pressed', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
const overview = within(rendered.getByTestId('logs-overview'));
|
||||
|
|
@ -480,7 +487,7 @@ describe('LogsPanel', () => {
|
|||
it('should toggle input and output panel when the button is clicked', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -510,7 +517,7 @@ describe('LogsPanel', () => {
|
|||
const workflow = deepCopy(aiChatWorkflow);
|
||||
workflow.id = 'test-workflow-id';
|
||||
setWorkflow(workflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
|
|
@ -535,7 +542,7 @@ describe('LogsPanel', () => {
|
|||
beforeEach(() => {
|
||||
logsStore.toggleOpen(true);
|
||||
setWorkflow(aiChatWorkflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
});
|
||||
|
||||
it('should allow to select previous and next row via keyboard shortcut', async () => {
|
||||
|
|
@ -594,7 +601,7 @@ describe('LogsPanel', () => {
|
|||
const workflow = deepCopy(aiChatWorkflow);
|
||||
workflow.id = 'test-workflow-id';
|
||||
setWorkflow(workflow);
|
||||
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
setExecutionData(aiChatExecutionResponse);
|
||||
|
||||
logsStore.toggleLogSelectionSync(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,6 @@ vi.mock('@/app/composables/useWorkflowHelpers', async (importOriginal) => {
|
|||
}),
|
||||
};
|
||||
});
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(() => ({
|
||||
setWorkflowExecutionData: vi.fn(),
|
||||
setActiveExecutionId: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
vi.mock('@/app/composables/useNodeHelpers', () => ({
|
||||
useNodeHelpers: vi.fn(() => ({
|
||||
updateNodesExecutionIssues: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
|
|||
import { restoreChatHistory } from '@/features/execution/logs/logs.utils';
|
||||
import { type INode, type INodeParameters, NodeHelpers } from 'n8n-workflow';
|
||||
import { isChatNode } from '@/app/utils/aiUtils';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { MessageComponentKey } from '@n8n/chat/constants/messageComponents';
|
||||
|
||||
|
|
@ -49,7 +48,6 @@ export function useChatState(
|
|||
const workflowExecutionState = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowState = injectWorkflowState();
|
||||
const rootStore = useRootStore();
|
||||
const logsStore = useLogsStore();
|
||||
const router = useRouter();
|
||||
|
|
@ -199,8 +197,8 @@ export function useChatState(
|
|||
}
|
||||
|
||||
// Clear any existing execution to allow fresh webhook registration
|
||||
workflowState.setWorkflowExecutionData(null);
|
||||
workflowState.setActiveExecutionId(undefined);
|
||||
workflowExecutionState.value.setWorkflowExecutionData(null);
|
||||
workflowExecutionState.value.setActiveExecutionId(undefined);
|
||||
|
||||
// Use the useRunWorkflow composable to properly register the webhook
|
||||
// Only include destinationNode if set for partial execution support
|
||||
|
|
@ -331,7 +329,7 @@ export function useChatState(
|
|||
);
|
||||
|
||||
function refreshSession() {
|
||||
workflowState.setWorkflowExecutionData(null);
|
||||
workflowExecutionState.value.setWorkflowExecutionData(null);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
logsStore.resetChatSessionId();
|
||||
logsStore.resetMessages();
|
||||
|
|
|
|||
|
|
@ -15,29 +15,17 @@ import {
|
|||
} from '@/__tests__/mocks';
|
||||
import { createRunExecutionData, type IRunExecutionData } from 'n8n-workflow';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { computed } from 'vue';
|
||||
|
||||
vi.mock('@/app/composables/useToast');
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
describe(useLogsExecutionData, () => {
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let workflowsListStore: ReturnType<typeof mockedStore<typeof useWorkflowsListStore>>;
|
||||
let nodeTypeStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
let executionStateStore: ReturnType<typeof useWorkflowExecutionStateStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
|
|
@ -45,8 +33,9 @@ describe(useLogsExecutionData, () => {
|
|||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsListStore = mockedStore(useWorkflowsListStore);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
// The composable resolves the execution-state store via the injected
|
||||
// document store (falls back to `workflowsStore.workflowId`, '' here).
|
||||
executionStateStore = useWorkflowExecutionStateStore(createWorkflowDocumentId(''));
|
||||
|
||||
nodeTypeStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypeStore.setNodeTypes(nodeTypes);
|
||||
|
|
@ -54,7 +43,7 @@ describe(useLogsExecutionData, () => {
|
|||
|
||||
describe('isEnabled', () => {
|
||||
beforeEach(() => {
|
||||
workflowState.setWorkflowExecutionData(
|
||||
executionStateStore.setWorkflowExecutionData(
|
||||
createTestWorkflowExecutionResponse({
|
||||
data: createRunExecutionData({ resultData: { runData: { n0: [createTestTaskData()] } } }),
|
||||
workflowData: createTestWorkflow({ nodes: [createTestNode({ name: 'n0' })] }),
|
||||
|
|
@ -81,7 +70,7 @@ describe(useLogsExecutionData, () => {
|
|||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
|
||||
workflowState.setWorkflowExecutionData(
|
||||
executionStateStore.setWorkflowExecutionData(
|
||||
createTestWorkflowExecutionResponse({
|
||||
id: 'e0',
|
||||
workflowData: createTestWorkflow({
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { isChatNode } from '@/app/utils/aiUtils';
|
|||
import { CHAT_TRIGGER_NODE_TYPE, LOGS_EXECUTION_DATA_THROTTLE_DURATION } from '@/app/constants';
|
||||
import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useThrottleWithReactiveDelay } from '@n8n/composables/useThrottleWithReactiveDelay';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
|
||||
|
|
@ -37,7 +36,6 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
|||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const toast = useToast();
|
||||
|
||||
const state = ref<
|
||||
|
|
@ -109,7 +107,9 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
|||
|
||||
function resetExecutionData() {
|
||||
state.value = undefined;
|
||||
workflowState.setWorkflowExecutionData(null);
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId).setWorkflowExecutionData(
|
||||
null,
|
||||
);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
// Clear partial execution destination to allow full workflow execution
|
||||
useWorkflowExecutionStateStore(
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import { setActivePinia } from 'pinia';
|
|||
import { computed, shallowRef } from 'vue';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
injectWorkflowDocumentStore,
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', async () => {
|
||||
const actual = await vi.importActual('@/app/stores/workflowDocument.store');
|
||||
|
|
@ -66,7 +67,7 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
|||
setActivePinia(pinia);
|
||||
|
||||
const workflow = createTestWorkflow({ nodes, connections });
|
||||
const workflowState = useWorkflowState();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
|
||||
workflowDocumentStore.hydrate(workflow);
|
||||
|
|
@ -78,7 +79,11 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
|||
}
|
||||
|
||||
if (runData) {
|
||||
workflowState.setWorkflowExecutionData({
|
||||
// The component reads run data via `workflowsStore.getWorkflowExecution`, which
|
||||
// resolves through the execution-state store keyed by `workflowsStore.workflowId`.
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData({
|
||||
id: '',
|
||||
workflowData: {
|
||||
id: '',
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import { useRouter } from 'vue-router';
|
|||
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
|
||||
|
||||
import { N8nIcon, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
type MappingMode = 'debugging' | 'mapping';
|
||||
|
||||
export type Props = {
|
||||
|
|
@ -112,7 +111,6 @@ const workflowDocumentStore = injectWorkflowDocumentStore();
|
|||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowState = injectWorkflowState();
|
||||
const router = useRouter();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const { canReveal, isDynamicCredentials, revealData } = useExecutionRedaction();
|
||||
|
|
@ -202,12 +200,12 @@ const isExecutingPrevious = computed(() => {
|
|||
return false;
|
||||
}
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
const executingNode = workflowState.executingNode.executingNode;
|
||||
const executingNode = workflowExecutionStateStore.value.executingNode.executingNode;
|
||||
|
||||
if (
|
||||
activeNode.value &&
|
||||
triggeredNode === activeNode.value.name &&
|
||||
workflowState.executingNode.isNodeExecuting(props.currentNodeName)
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(props.currentNodeName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -215,7 +213,8 @@ const isExecutingPrevious = computed(() => {
|
|||
if (executingNode.length || triggeredNode) {
|
||||
return !!parentNodes.value.find(
|
||||
(node) =>
|
||||
workflowState.executingNode.isNodeExecuting(node.name) || node.name === triggeredNode,
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.name) ||
|
||||
node.name === triggeredNode,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import RedactedDataState from '@/features/ndv/panel/components/RedactedDataState
|
|||
import NodeExecuteButton from '@/app/components/NodeExecuteButton.vue';
|
||||
|
||||
import { N8nIcon, N8nRadioButtons, N8nSpinner, N8nText } from '@n8n/design-system';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/app/constants';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
|
@ -81,7 +80,6 @@ const workflowId = useInjectWorkflowId();
|
|||
const ndvStore = injectNDVStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
|
|
@ -162,7 +160,7 @@ const isNodeRunning = computed(() => {
|
|||
return (
|
||||
workflowRunning.value &&
|
||||
!!node.value &&
|
||||
workflowState.executingNode.isNodeExecuting(node.value.name)
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.value.name)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||
import NodeSettings from './NodeSettings.vue';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
injectWorkflowDocumentStore,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', async () => {
|
||||
const actual = await vi.importActual('@/app/stores/workflowDocument.store');
|
||||
|
|
@ -54,7 +54,6 @@ const renderNodeSettings = (runData?: IRunData) => {
|
|||
|
||||
const workflow = createTestWorkflow({ nodes: [httpNode], connections: {} });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = useWorkflowState();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
workflowsStore.setWorkflowId(workflow.id);
|
||||
const ndvStore = useNDVStore(createWorkflowDocumentId(workflow.id));
|
||||
|
|
@ -65,7 +64,7 @@ const renderNodeSettings = (runData?: IRunData) => {
|
|||
ndvStore.activeNodeName = httpNode.name;
|
||||
|
||||
if (runData) {
|
||||
workflowState.setWorkflowExecutionData({
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId(workflow.id)).setWorkflowExecutionData({
|
||||
id: 'exec-1',
|
||||
workflowData: {
|
||||
...workflow,
|
||||
|
|
|
|||
|
|
@ -33,14 +33,6 @@ vi.mock('@/app/composables/useTelemetry', () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(CommunityPlusEnrollmentModal, {
|
||||
global: {
|
||||
stubs: {
|
||||
|
|
|
|||
|
|
@ -32,16 +32,6 @@ vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const { mockUpdateNodeProperties } = vi.hoisted(() => ({
|
||||
mockUpdateNodeProperties: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(() => ({
|
||||
updateNodeProperties: mockUpdateNodeProperties,
|
||||
})),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(SetupCardBody);
|
||||
|
||||
const NODE_PROPERTIES = [
|
||||
|
|
@ -82,7 +72,6 @@ describe('SetupCardBody', () => {
|
|||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUpdateNodeProperties.mockClear();
|
||||
createTestingPinia();
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({ properties: NODE_PROPERTIES });
|
||||
|
|
|
|||
|
|
@ -40,11 +40,6 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { MarkerType } from '@vue-flow/core';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import {
|
||||
injectWorkflowState,
|
||||
useWorkflowState,
|
||||
type WorkflowState,
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
|
|
@ -100,16 +95,6 @@ function createRenderDataWithExecutionIssuesByNodeName(
|
|||
});
|
||||
}
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let workflowState: WorkflowState;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: false,
|
||||
|
|
@ -143,9 +128,6 @@ beforeEach(() => {
|
|||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
renderNodeInputsMap.clear();
|
||||
renderNodeOutputsMap.clear();
|
||||
|
||||
|
|
@ -174,6 +156,12 @@ function setWorkflowRunning(running: boolean) {
|
|||
vi.spyOn(workflowExecutionStateStore, 'isWorkflowRunning', 'get').mockReturnValue(running);
|
||||
}
|
||||
|
||||
function getExecutingNode() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
return useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
.executingNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the render data maps directly for tests
|
||||
* that rely on per-node inputs/outputs from the render data.
|
||||
|
|
@ -230,7 +218,7 @@ describe('useCanvasMapping', () => {
|
|||
nodes,
|
||||
connections,
|
||||
});
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(false);
|
||||
getExecutingNode().isNodeExecuting = vi.fn().mockReturnValue(false);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
|
|
@ -329,7 +317,7 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
getExecutingNode().isNodeExecuting = vi.fn().mockReturnValue(true);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
|
|
@ -1789,8 +1777,8 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.executingNode = [];
|
||||
workflowState.executingNode.lastAddedExecutingNode = node1.name;
|
||||
getExecutingNode().executingNode = [];
|
||||
getExecutingNode().lastAddedExecutingNode = node1.name;
|
||||
setWorkflowRunning(true);
|
||||
|
||||
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
|
||||
|
|
@ -1819,8 +1807,8 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.executingNode = [];
|
||||
workflowState.executingNode.lastAddedExecutingNode = node1.name;
|
||||
getExecutingNode().executingNode = [];
|
||||
getExecutingNode().lastAddedExecutingNode = node1.name;
|
||||
setWorkflowRunning(false);
|
||||
|
||||
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
|
||||
|
|
@ -1849,8 +1837,8 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.executingNode = [node2.name];
|
||||
workflowState.executingNode.lastAddedExecutingNode = node1.name;
|
||||
getExecutingNode().executingNode = [node2.name];
|
||||
getExecutingNode().lastAddedExecutingNode = node1.name;
|
||||
setWorkflowRunning(false);
|
||||
|
||||
const { nodeExecutionWaitingForNextById } = useCanvasMapping({
|
||||
|
|
@ -2336,11 +2324,9 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.isNodeExecuting = vi
|
||||
.fn()
|
||||
.mockImplementation((nodeName: string) => {
|
||||
return nodeName === manualTriggerNode.name;
|
||||
});
|
||||
getExecutingNode().isNodeExecuting = vi.fn().mockImplementation((nodeName: string) => {
|
||||
return nodeName === manualTriggerNode.name;
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === manualTriggerNode.name) {
|
||||
|
|
@ -2919,11 +2905,9 @@ describe('useCanvasMapping', () => {
|
|||
connections,
|
||||
});
|
||||
|
||||
workflowState.executingNode.isNodeExecuting = vi
|
||||
.fn()
|
||||
.mockImplementation((nodeName: string) => {
|
||||
return nodeName === manualTriggerNode.name;
|
||||
});
|
||||
getExecutingNode().isNodeExecuting = vi.fn().mockImplementation((nodeName: string) => {
|
||||
return nodeName === manualTriggerNode.name;
|
||||
});
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ import { useNodeDirtiness } from '@/app/composables/useNodeDirtiness';
|
|||
import { getNodeIconSource } from '@/app/utils/nodeIcon';
|
||||
import * as workflowUtils from 'n8n-workflow/common';
|
||||
import { throttledWatch } from '@vueuse/core';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types';
|
||||
|
||||
export function useCanvasMapping({
|
||||
|
|
@ -75,7 +74,6 @@ export function useCanvasMapping({
|
|||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowState = injectWorkflowState();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const { dirtinessByName } = useNodeDirtiness();
|
||||
|
|
@ -249,7 +247,7 @@ export function useCanvasMapping({
|
|||
|
||||
const nodeExecutionRunningById = computed(() =>
|
||||
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||
acc[node.id] = workflowState.executingNode.isNodeExecuting(node.name);
|
||||
acc[node.id] = workflowExecutionStateStore.value.executingNode.isNodeExecuting(node.name);
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
|
@ -257,8 +255,8 @@ export function useCanvasMapping({
|
|||
const nodeExecutionWaitingForNextById = computed(() =>
|
||||
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||
acc[node.id] =
|
||||
node.name === workflowState.executingNode.lastAddedExecutingNode &&
|
||||
workflowState.executingNode.executingNode.length === 0 &&
|
||||
node.name === workflowExecutionStateStore.value.executingNode.lastAddedExecutingNode &&
|
||||
workflowExecutionStateStore.value.executingNode.executingNode.length === 0 &&
|
||||
workflowExecutionStateStore.value.isWorkflowRunning;
|
||||
|
||||
return acc;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user