mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 12:51:16 +08:00
refactor(editor): Migrate whole workflow object consumers (#29395)
This commit is contained in:
@@ -14,7 +14,6 @@ import { useExecutionsStore } from '@/features/execution/executions/executions.s
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { computed, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import type { RouteLocation, RouteLocationRaw } from 'vue-router';
|
||||
@@ -35,7 +34,6 @@ const pushConnection = usePushConnection({ router });
|
||||
const toast = useToast();
|
||||
const ndvStore = useNDVStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const executionsStore = useExecutionsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -71,7 +69,6 @@ const activeNode = computed(() => ndvStore.activeNode);
|
||||
const hideMenuBar = computed(() =>
|
||||
Boolean(activeNode.value && activeNode.value.type !== STICKY_NODE_TYPE),
|
||||
);
|
||||
const workflow = computed(() => workflowsStore.workflow);
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null);
|
||||
const workflowName = computed(() => workflowDocumentStore?.value?.name ?? '');
|
||||
@@ -265,7 +262,7 @@ async function onWorkflowDeactivated() {
|
||||
) {
|
||||
try {
|
||||
// Fetch the updated workflow to get the latest settings after backend processing
|
||||
const updatedWorkflow = await workflowsListStore.fetchWorkflow(workflow.value.id);
|
||||
const updatedWorkflow = await workflowsListStore.fetchWorkflow(workflowId.value);
|
||||
workflowDocumentStore?.value?.hydrate(updatedWorkflow);
|
||||
toast.showToast({
|
||||
title: locale.baseText('mcp.workflowDeactivated.title'),
|
||||
@@ -291,7 +288,7 @@ async function onWorkflowDeactivated() {
|
||||
<div v-show="!hideMenuBar && !settingsStore.isCanvasOnly" :class="$style['top-menu']">
|
||||
<WorkflowDetails
|
||||
v-if="workflowName"
|
||||
:id="workflow.id"
|
||||
:id="workflowId"
|
||||
:tags="workflowTags"
|
||||
:name="workflowName"
|
||||
:current-folder="parentFolderForBreadcrumbs"
|
||||
|
||||
@@ -497,7 +497,7 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
|
||||
<ConnectionTracker class="actions">
|
||||
<WorkflowProductionChecklist v-if="!isNewWorkflow" :workflow="workflowsStore.workflow" />
|
||||
<WorkflowProductionChecklist v-if="!isNewWorkflow" />
|
||||
<WorkflowHeaderDraftPublishActions
|
||||
:id="id"
|
||||
ref="workflowHeaderActions"
|
||||
|
||||
+72
-266
@@ -161,6 +161,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
createWorkflowDocumentId(mockWorkflow.id),
|
||||
);
|
||||
workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null });
|
||||
workflowDocumentStore.setSettings(mockWorkflow.settings ?? { executionOrder: 'v1' });
|
||||
workflowDocumentStoreRef.value = workflowDocumentStore;
|
||||
|
||||
router = {
|
||||
@@ -203,16 +204,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
activeVersion: null,
|
||||
});
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
active: false,
|
||||
activeVersionId: null,
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
const { container } = renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-test-id="n8n-suggested-actions-stub"]'),
|
||||
@@ -222,12 +214,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
it('should not render when cache is loading', () => {
|
||||
workflowsCache.isCacheLoading.value = true;
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
const { container } = renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-test-id="n8n-suggested-actions-stub"]'),
|
||||
@@ -244,15 +231,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
// @ts-expect-error - mocking readonly property
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })],
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: 'ai-node', typeVersion: 1 }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -292,15 +275,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
.fn()
|
||||
.mockReturnValue(mockNonAINodeType as INodeTypeDescription);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [createTestNode({ type: 'regular-node', typeVersion: 1 })],
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: 'regular-node', typeVersion: 1 }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -323,12 +302,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should show error workflow action and time saved when not ignored', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -359,12 +333,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
const { container } = renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
@@ -374,15 +343,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should not show error workflow action when workflow contains an enabled Error Trigger node', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1 })],
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1 }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -398,17 +363,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should show error workflow action when workflow contains a disabled Error Trigger node', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [
|
||||
createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1, disabled: true }),
|
||||
],
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: ERROR_TRIGGER_NODE_TYPE, typeVersion: 1, disabled: true }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -441,15 +400,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
// @ts-expect-error - mocking readonly property
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })],
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: 'ai-node', typeVersion: 1 }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -470,12 +425,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
uiStore = useUIStore(pinia);
|
||||
const openModalSpy = vi.spyOn(uiStore, 'openModal');
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -493,12 +443,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
uiStore = useUIStore(pinia);
|
||||
const openModalSpy = vi.spyOn(uiStore, 'openModal');
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -512,12 +457,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should ignore specific action when ignore is clicked', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -537,12 +477,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should ignore all actions after confirmation', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -568,12 +503,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
it('should not ignore all actions if confirmation is cancelled', async () => {
|
||||
message.confirm = vi.fn().mockResolvedValue('cancel');
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -593,12 +523,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
|
||||
describe('Popover behavior', () => {
|
||||
it('should track when popover is opened via update:open event', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -622,16 +547,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
activeVersion: null,
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
active: false,
|
||||
activeVersionId: null,
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
workflowDocumentStoreRef.value?.setActiveState({
|
||||
activeVersionId: 'v1',
|
||||
@@ -658,16 +574,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
activeVersion: null,
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
active: false,
|
||||
activeVersionId: null,
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
@@ -703,16 +610,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
activeVersion: null,
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
active: false,
|
||||
activeVersionId: null,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
@@ -740,12 +638,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -768,15 +661,11 @@ describe('WorkflowProductionChecklist', () => {
|
||||
// @ts-expect-error - mocking readonly property
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(mockAINodeType as INodeTypeDescription);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
nodes: [createTestNode({ type: 'ai-node', typeVersion: 1 })],
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
workflowDocumentStoreRef.value?.setNodes([
|
||||
createTestNode({ type: 'ai-node', typeVersion: 1 }),
|
||||
]);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -806,19 +695,13 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should mark error workflow as completed when error workflow is set', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
errorWorkflow: 'error-workflow-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
workflowDocumentStoreRef.value?.setSettings({
|
||||
executionOrder: 'v1',
|
||||
errorWorkflow: 'error-workflow-id',
|
||||
});
|
||||
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
{
|
||||
@@ -840,19 +723,13 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
|
||||
it('should mark time saved as completed when time saved is set', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
timeSavedPerExecution: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
workflowDocumentStoreRef.value?.setSettings({
|
||||
executionOrder: 'v1',
|
||||
timeSavedPerExecution: 10,
|
||||
});
|
||||
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
{
|
||||
@@ -885,17 +762,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
workflowDocumentStoreRef.value = workflowDocumentStore;
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
},
|
||||
},
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
renderComponent({ pinia: createTestingPinia() });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -924,12 +791,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
settingsStore = useSettingsStore(pinia);
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(false);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toEqual([
|
||||
@@ -962,12 +824,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(true);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toContainEqual({
|
||||
@@ -992,12 +849,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(false);
|
||||
vi.spyOn(usersStore, 'isInstanceOwner', 'get').mockReturnValue(false);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const actions = mockN8nSuggestedActionsProps.actions;
|
||||
@@ -1023,12 +875,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const actions = mockN8nSuggestedActionsProps.actions;
|
||||
@@ -1046,12 +893,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false },
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toContainEqual({
|
||||
@@ -1073,19 +915,13 @@ describe('WorkflowProductionChecklist', () => {
|
||||
mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false },
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: {
|
||||
...mockWorkflow,
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
availableInMCP: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
workflowDocumentStoreRef.value?.setSettings({
|
||||
executionOrder: 'v1',
|
||||
availableInMCP: true,
|
||||
});
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toContainEqual({
|
||||
id: 'workflow-mcp-access',
|
||||
@@ -1112,12 +948,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const actions = mockN8nSuggestedActionsProps.actions;
|
||||
@@ -1137,12 +968,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
});
|
||||
vi.spyOn(usersStore, 'isAdmin', 'get').mockReturnValue(true);
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -1166,12 +992,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
mcp: { mcpAccessEnabled: true, mcpManagedByEnv: false },
|
||||
});
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -1194,12 +1015,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
branchReadOnly: true,
|
||||
} as SourceControlPreferences;
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -1217,12 +1033,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
branchReadOnly: false,
|
||||
} as SourceControlPreferences;
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
@@ -1236,12 +1047,7 @@ describe('WorkflowProductionChecklist', () => {
|
||||
|
||||
sourceControlStore.preferences = {} as SourceControlPreferences;
|
||||
|
||||
renderComponent({
|
||||
props: {
|
||||
workflow: mockWorkflow,
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
renderComponent({ pinia });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockN8nSuggestedActionsProps.actions).toBeDefined();
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { ActionType, WorkflowSettings } from '@/app/composables/useWorkflowsCache';
|
||||
import { useWorkflowSettingsCache } from '@/app/composables/useWorkflowsCache';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import {
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
@@ -29,10 +28,6 @@ import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
const props = defineProps<{
|
||||
workflow: IWorkflowDb;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const evaluationStore = useEvaluationStore();
|
||||
@@ -49,9 +44,9 @@ const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null);
|
||||
const isPopoverOpen = ref(false);
|
||||
const cachedSettings = ref<WorkflowSettings | null>(null);
|
||||
|
||||
const nodes = computed(() => workflowDocumentStore?.value?.allNodes ?? []);
|
||||
const hasAINode = computed(() => {
|
||||
const nodes = props.workflow.nodes;
|
||||
return nodes.some((node) => {
|
||||
return nodes.value.some((node) => {
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
return nodeType?.codex?.categories?.includes('AI');
|
||||
});
|
||||
@@ -62,28 +57,22 @@ const hasEvaluationSetOutputsNode = computed((): boolean => {
|
||||
});
|
||||
|
||||
const hasErrorWorkflow = computed(() => {
|
||||
const errorWorkflow =
|
||||
workflowDocumentStore?.value?.settings?.errorWorkflow ?? props.workflow.settings?.errorWorkflow;
|
||||
const errorWorkflow = workflowDocumentStore?.value?.settings?.errorWorkflow;
|
||||
return !!errorWorkflow;
|
||||
});
|
||||
|
||||
const isErrorWorkflow = computed(() => {
|
||||
return props.workflow.nodes.some(
|
||||
return nodes.value.some(
|
||||
(node) => node.type === ERROR_TRIGGER_NODE_TYPE && node.disabled !== true,
|
||||
);
|
||||
});
|
||||
|
||||
const hasSavedTimeNodes = computed(() => {
|
||||
if (!props.workflow?.nodes) return false;
|
||||
return props.workflow.nodes.some(
|
||||
(node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true,
|
||||
);
|
||||
return nodes.value.some((node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true);
|
||||
});
|
||||
|
||||
const hasTimeSaved = computed(() => {
|
||||
const timeSavedPerExecution =
|
||||
workflowDocumentStore?.value?.settings?.timeSavedPerExecution ??
|
||||
props.workflow.settings?.timeSavedPerExecution;
|
||||
const timeSavedPerExecution = workflowDocumentStore?.value?.settings?.timeSavedPerExecution;
|
||||
return timeSavedPerExecution !== undefined || hasSavedTimeNodes.value;
|
||||
});
|
||||
|
||||
@@ -214,18 +203,17 @@ const availableActions = computed(() => {
|
||||
...baseAction,
|
||||
id: 'workflow-mcp-access',
|
||||
description: i18n.baseText('mcp.productionChecklist.workflow.description'),
|
||||
completed:
|
||||
workflowDocumentStore?.value?.settings?.availableInMCP ??
|
||||
props.workflow.settings?.availableInMCP ??
|
||||
false,
|
||||
completed: workflowDocumentStore?.value?.settings?.availableInMCP ?? false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function loadWorkflowSettings() {
|
||||
if (props.workflow.id) {
|
||||
if (workflowDocumentStore?.value?.workflowId) {
|
||||
// todo add global config
|
||||
cachedSettings.value = await workflowsCache.getMergedWorkflowSettings(props.workflow.id);
|
||||
cachedSettings.value = await workflowsCache.getMergedWorkflowSettings(
|
||||
workflowDocumentStore?.value.workflowId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +223,7 @@ async function handleActionClick(actionId: string) {
|
||||
// Navigate to evaluations
|
||||
await router.push({
|
||||
name: VIEWS.EVALUATION_EDIT,
|
||||
params: { workflowId: props.workflow.id },
|
||||
params: { workflowId: workflowDocumentStore?.value?.workflowId },
|
||||
});
|
||||
break;
|
||||
case 'errorWorkflow':
|
||||
@@ -269,7 +257,10 @@ async function handleIgnoreClick(actionId: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
await workflowsCache.ignoreSuggestedAction(props.workflow.id, actionId);
|
||||
await workflowsCache.ignoreSuggestedAction(
|
||||
workflowDocumentStore?.value?.workflowId ?? '',
|
||||
actionId,
|
||||
);
|
||||
await loadWorkflowSettings();
|
||||
|
||||
telemetry.track('user clicked ignore suggested action', {
|
||||
@@ -326,7 +317,7 @@ watch(
|
||||
}
|
||||
|
||||
// Update firstActivatedAt after opening popover
|
||||
await workflowsCache.updateFirstActivatedAt(props.workflow.id);
|
||||
await workflowsCache.updateFirstActivatedAt(workflowDocumentStore?.value?.workflowId ?? '');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,6 +17,18 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsEEStore } from '@/app/stores/workflows.ee.store';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import { useRolesStore } from '@/app/stores/roles.store';
|
||||
import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types';
|
||||
|
||||
const mockWorkflowDocumentState = reactive({
|
||||
homeProject: null as ProjectSharingData | null,
|
||||
scopes: [] as string[],
|
||||
sharedWithProjects: [] as ProjectSharingData[],
|
||||
name: '',
|
||||
});
|
||||
vi.mock('@/app/stores/workflowDocument.store', () => ({
|
||||
useWorkflowDocumentStore: () => mockWorkflowDocumentState,
|
||||
createWorkflowDocumentId: (id: string) => `${id}@latest`,
|
||||
}));
|
||||
|
||||
const mockRouteQuery = reactive<Record<string, string>>({});
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
@@ -73,7 +85,6 @@ let workflowsStore: MockedStore<typeof useWorkflowsStore>;
|
||||
let workflowsEEStore: MockedStore<typeof useWorkflowsEEStore>;
|
||||
let projectsStore: MockedStore<typeof useProjectsStore>;
|
||||
let rolesStore: MockedStore<typeof useRolesStore>;
|
||||
|
||||
describe('WorkflowShareModal.ee.vue', () => {
|
||||
beforeEach(() => {
|
||||
settingsStore = mockedStore(useSettingsStore);
|
||||
@@ -84,6 +95,10 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
|
||||
// Reset route query
|
||||
Object.keys(mockRouteQuery).forEach((key) => delete mockRouteQuery[key]);
|
||||
mockWorkflowDocumentState.homeProject = null;
|
||||
mockWorkflowDocumentState.sharedWithProjects = [];
|
||||
mockWorkflowDocumentState.scopes = [];
|
||||
mockWorkflowDocumentState.name = '';
|
||||
|
||||
// Set up default store state
|
||||
settingsStore.settings.enterprise = { sharing: true } as FrontendSettings['enterprise'];
|
||||
@@ -121,6 +136,15 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
// Set route query to indicate new workflow
|
||||
mockRouteQuery.new = 'true';
|
||||
|
||||
const homeProject: ProjectSharingData = {
|
||||
id: 'personal-project-id',
|
||||
name: 'Personal Project',
|
||||
type: ProjectTypes.Personal,
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
workflowsStore.workflow = {
|
||||
id: '',
|
||||
name: 'My workflow',
|
||||
@@ -133,8 +157,11 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
scopes: [],
|
||||
nodes: [],
|
||||
connections: {},
|
||||
homeProject,
|
||||
};
|
||||
|
||||
mockWorkflowDocumentState.homeProject = homeProject;
|
||||
|
||||
const saveWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'saveWorkflowSharedWith');
|
||||
|
||||
const props = {
|
||||
@@ -174,6 +201,15 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
type: ProjectTypes.Personal,
|
||||
});
|
||||
|
||||
const homeProject: ProjectSharingData = {
|
||||
id: 'personal-project-id',
|
||||
name: 'Personal Project',
|
||||
type: ProjectTypes.Personal,
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
workflowsStore.workflow = {
|
||||
id: 'workflow-1',
|
||||
name: 'My workflow',
|
||||
@@ -186,16 +222,11 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
scopes: [],
|
||||
nodes: [],
|
||||
connections: {},
|
||||
homeProject: {
|
||||
id: 'personal-project-id',
|
||||
name: 'Personal Project',
|
||||
type: ProjectTypes.Personal,
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
homeProject,
|
||||
};
|
||||
|
||||
mockWorkflowDocumentState.homeProject = homeProject;
|
||||
|
||||
const props = {
|
||||
data: { id: 'workflow-1' },
|
||||
};
|
||||
|
||||
@@ -66,10 +66,7 @@ const workflowName = computed(
|
||||
() => workflowListEntry.value?.name ?? workflowDocumentStore.value.name,
|
||||
);
|
||||
const workflowHomeProject = computed(
|
||||
() =>
|
||||
workflowListEntry.value?.homeProject ??
|
||||
workflowDocumentStore.value.homeProject ??
|
||||
workflowsStore.workflow.homeProject,
|
||||
() => workflowListEntry.value?.homeProject ?? workflowDocumentStore.value.homeProject,
|
||||
);
|
||||
const workflowScopes = computed(
|
||||
() => workflowListEntry.value?.scopes ?? workflowDocumentStore.value.scopes,
|
||||
@@ -304,6 +301,7 @@ watch(
|
||||
<EnterpriseEdition :features="[EnterpriseEditionFeature.Sharing]" :class="$style.content">
|
||||
<div>
|
||||
<ProjectSharing
|
||||
v-if="workflowHomeProject"
|
||||
v-model="sharedWithProjects"
|
||||
:home-project="workflowHomeProject"
|
||||
:search-fn="searchFn"
|
||||
|
||||
@@ -221,7 +221,7 @@ describe('useCanvasOperations', () => {
|
||||
// Tests that need custom behavior can override via vi.spyOn.
|
||||
vi.mocked(workflowDocumentStoreInstance.getParentNodesByDepth).mockReturnValue([]);
|
||||
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
|
||||
vi.mocked(workflowDocumentStoreInstance.getSnapshot).mockReturnValue({
|
||||
vi.mocked(workflowDocumentStoreInstance.getWorkflowObjectAccessorSnapshot).mockReturnValue({
|
||||
id: workflowDocumentStoreInstance.workflowId,
|
||||
connectionsBySourceNode: workflowDocumentStoreInstance.connectionsBySourceNode,
|
||||
pinData: workflowDocumentStoreInstance.pinData as IPinData,
|
||||
|
||||
@@ -211,10 +211,10 @@ export function useCanvasOperations() {
|
||||
|
||||
const preventOpeningNDV = !!localStorage.getItem('NodeView.preventOpeningNDV');
|
||||
|
||||
const editableWorkflow = computed<IWorkflowDb>(() => workflowsStore.workflow);
|
||||
|
||||
const editableWorkflowObject = computed(() =>
|
||||
workflowDocumentStore.value ? workflowDocumentStore.value.getSnapshot() : undefined,
|
||||
workflowDocumentStore.value
|
||||
? workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot()
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const triggerNodes = computed<INodeUi[]>(() => {
|
||||
@@ -2708,12 +2708,16 @@ export function useCanvasOperations() {
|
||||
// the user
|
||||
workflowHelpers.updateNodePositions(
|
||||
workflowData,
|
||||
NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value, {
|
||||
...(workflowData.nodes && workflowData.nodes.length > 1
|
||||
? { size: getNodesGroupSize(workflowData.nodes) }
|
||||
: {}),
|
||||
viewport,
|
||||
}),
|
||||
NodeViewUtils.getNewNodePosition(
|
||||
workflowDocumentStore.value.allNodes,
|
||||
lastClickPosition.value,
|
||||
{
|
||||
...(workflowData.nodes && workflowData.nodes.length > 1
|
||||
? { size: getNodesGroupSize(workflowData.nodes) }
|
||||
: {}),
|
||||
viewport,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await addImportedNodesToWorkflow(workflowData, {
|
||||
@@ -3076,10 +3080,11 @@ export function useCanvasOperations() {
|
||||
telemetry: true,
|
||||
});
|
||||
|
||||
const offsetIndex = editableWorkflow.value.nodes.length - nodes.length;
|
||||
const allNodes = workflowDocumentStore.value.allNodes;
|
||||
const offsetIndex = allNodes.length - nodes.length;
|
||||
const connections: CanvasConnectionCreateData[] = addedConnections.map(({ from, to }) => {
|
||||
const fromNode = editableWorkflow.value.nodes[offsetIndex + from.nodeIndex];
|
||||
const toNode = editableWorkflow.value.nodes[offsetIndex + to.nodeIndex];
|
||||
const fromNode = allNodes[offsetIndex + from.nodeIndex];
|
||||
const toNode = allNodes[offsetIndex + to.nodeIndex];
|
||||
const type = from.type ?? to.type ?? NodeConnectionTypes.Main;
|
||||
|
||||
return {
|
||||
@@ -3251,7 +3256,6 @@ export function useCanvasOperations() {
|
||||
|
||||
return {
|
||||
lastClickPosition,
|
||||
editableWorkflow,
|
||||
editableWorkflowObject,
|
||||
triggerNodes,
|
||||
requireNodeTypeDescription,
|
||||
|
||||
@@ -15,6 +15,12 @@ import type { JSONSchema7 } from 'json-schema';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
vi.mock('@/app/stores/workflows.store');
|
||||
vi.mock('@/app/stores/workflowDocument.store', () => ({
|
||||
createWorkflowDocumentId: vi.fn(() => 'test'),
|
||||
useWorkflowDocumentStore: vi.fn(() => ({
|
||||
getSettingsSnapshot: () => ({ binaryMode: undefined }),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useDataSchema', () => {
|
||||
const getSchema = useDataSchema().getSchema;
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
type ITaskDataConnections,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { type IconName } from '@n8n/design-system/components/N8nIcon/icons';
|
||||
import { DATA_TYPE_ICON_MAP } from '@/app/constants';
|
||||
import { DEFAULT_SETTINGS } from '../stores/workflowDocument/useWorkflowDocumentSettings';
|
||||
|
||||
export function useDataSchema() {
|
||||
function getSchema(
|
||||
@@ -554,6 +555,11 @@ export const useFlattenSchema = () => {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
|
||||
acc = acc.concat(
|
||||
flattenSchema({
|
||||
isDataEmpty: item.isDataEmpty,
|
||||
@@ -567,7 +573,9 @@ export const useFlattenSchema = () => {
|
||||
expressionPrefix: getNodeParentExpression({
|
||||
nodeName: item.node.name,
|
||||
distanceFromActive: item.depth,
|
||||
binaryMode: useWorkflowsStore().workflow.settings?.binaryMode,
|
||||
binaryMode:
|
||||
workflowDocumentStore.value.getSettingsSnapshot().binaryMode ??
|
||||
DEFAULT_SETTINGS.binaryMode,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -288,7 +288,7 @@ export function useNodeHelpers() {
|
||||
}
|
||||
|
||||
const nodeInputIssues = getNodeInputIssues(
|
||||
workflowDocumentStore.value.getSnapshot(),
|
||||
workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
node,
|
||||
nodeType,
|
||||
);
|
||||
|
||||
@@ -178,7 +178,8 @@ export function usePinnedData(
|
||||
|
||||
if (typeof data === 'object') data = JSON.stringify(data);
|
||||
|
||||
const { pinData: _pinData, ...workflowObjectWithoutPinData } = workflowsStore.workflow;
|
||||
const { pinData: _pinData, ...workflowObjectWithoutPinData } =
|
||||
workflowDocumentStore.value?.getSnapshot() ?? {};
|
||||
const currentPinData = (workflowDocumentStore.value?.pinData ?? {}) as IPinData;
|
||||
const workflowJson = jsonStringify(workflowObjectWithoutPinData, { replaceCircularRefs: true });
|
||||
|
||||
|
||||
+5
-1
@@ -207,6 +207,10 @@ export async function fetchExecutionData(
|
||||
executionId: string,
|
||||
): Promise<SimplifiedExecution | undefined> {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
try {
|
||||
const executionResponse = await workflowsStore.fetchExecutionDataById(executionId);
|
||||
if (!executionResponse?.data) {
|
||||
@@ -216,7 +220,7 @@ export async function fetchExecutionData(
|
||||
return {
|
||||
id: executionId,
|
||||
workflowId: executionResponse.workflowId,
|
||||
workflowData: workflowsStore.workflow,
|
||||
workflowData: workflowDocumentStore.getSnapshot(),
|
||||
data: executionResponse.data,
|
||||
status: executionResponse.status,
|
||||
startedAt: workflowsStore.workflowExecutionData?.startedAt as Date,
|
||||
|
||||
+5
-16
@@ -38,10 +38,10 @@ export async function executionStarted(
|
||||
// Initialize or reinitialize workflowExecutionData to clear previous execution's
|
||||
// node status (e.g. DemoLayout iframe receiving push events for a new execution).
|
||||
if (!workflowsStore.workflowExecutionData?.data || needsInit) {
|
||||
const wf = workflowsStore.workflow;
|
||||
const workflowDocumentStore = workflowsStore.workflowId
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
: undefined;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
options.workflowState.setWorkflowExecutionData({
|
||||
id: data.executionId,
|
||||
finished: false,
|
||||
@@ -49,18 +49,7 @@ export async function executionStarted(
|
||||
status: 'running',
|
||||
createdAt: new Date(),
|
||||
startedAt: new Date(),
|
||||
workflowData: {
|
||||
id: wf.id,
|
||||
name: workflowDocumentStore?.name ?? '',
|
||||
active: wf.active,
|
||||
isArchived: wf.isArchived,
|
||||
nodes: wf.nodes,
|
||||
connections: wf.connections,
|
||||
createdAt: wf.createdAt,
|
||||
updatedAt: wf.updatedAt,
|
||||
versionId: wf.versionId ?? '',
|
||||
activeVersionId: wf.activeVersionId ?? null,
|
||||
},
|
||||
workflowData: workflowDocumentStore.getSnapshot(),
|
||||
data: createRunExecutionData(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@/app/composables/useWorkflowState';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import { useChat } from '@n8n/chat/composables';
|
||||
import type { IStartRunData } from '@/Interface';
|
||||
import type { INodeUi, IStartRunData } from '@/Interface';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
@@ -43,39 +43,45 @@ import {
|
||||
CHAT_HITL_TOOL_NODE_TYPE,
|
||||
} from '../constants';
|
||||
import type { WorkflowObjectAccessors } from '../types';
|
||||
import type { useWorkflowDocumentStore } from '../stores/workflowDocument.store';
|
||||
import type { Mocked } from 'vitest';
|
||||
|
||||
type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||
|
||||
const { mockDocumentStore } = vi.hoisted(() => {
|
||||
const store = {
|
||||
workflowId: '123',
|
||||
name: 'Test Workflow',
|
||||
allNodes: [] as unknown[],
|
||||
allNodes: [],
|
||||
getNodeByName: vi.fn(),
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
getChildNodes: vi.fn().mockReturnValue([]),
|
||||
getStartNode: vi.fn(),
|
||||
checkIfNodeHasChatParent: vi.fn(),
|
||||
checkIfToolNodeHasChatParent: vi.fn(),
|
||||
connectionsBySourceNode: {} as Record<string, unknown>,
|
||||
pinData: {} as Record<string, unknown>,
|
||||
connectionsBySourceNode: {},
|
||||
pinData: {},
|
||||
incomingConnectionsByNodeName: vi.fn().mockReturnValue({}),
|
||||
outgoingConnectionsByNodeName: vi.fn().mockReturnValue({}),
|
||||
nodesIssuesExist: false,
|
||||
getParametersLastUpdate: vi.fn(),
|
||||
getPinnedDataLastUpdate: vi.fn(),
|
||||
getPinnedDataLastRemovedAt: vi.fn(),
|
||||
getSnapshot: vi.fn(),
|
||||
getWorkflowObjectAccessorSnapshot: vi.fn(),
|
||||
hasNodeValidationIssues: false,
|
||||
nodeValidationIssues: [],
|
||||
serialize: vi.fn(),
|
||||
};
|
||||
store.getSnapshot.mockReturnValue({
|
||||
} as Partial<Mocked<Writable<ReturnType<typeof useWorkflowDocumentStore>>>> as Mocked<
|
||||
Writable<ReturnType<typeof useWorkflowDocumentStore>>
|
||||
>;
|
||||
store.getWorkflowObjectAccessorSnapshot.mockReturnValue({
|
||||
id: store.workflowId,
|
||||
getNode: store.getNodeByName,
|
||||
getParentNodes: store.getParentNodes,
|
||||
getChildNodes: store.getChildNodes,
|
||||
connectionsBySourceNode: store.connectionsBySourceNode,
|
||||
pinData: store.pinData,
|
||||
});
|
||||
pinData: store.pinData as IPinData,
|
||||
} as Partial<WorkflowObjectAccessors> as WorkflowObjectAccessors);
|
||||
return { mockDocumentStore: store };
|
||||
});
|
||||
|
||||
@@ -433,7 +439,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
return [];
|
||||
});
|
||||
vi.mocked(mockDocumentStore.getNodeByName).mockImplementation((name: string) => {
|
||||
const nodes: Record<string, unknown> = {
|
||||
const nodes: Record<string, INodeUi> = {
|
||||
[parentNodeName]: createTestNode({ name: parentNodeName }),
|
||||
[destinationNodeName]: createTestNode({ name: destinationNodeName }),
|
||||
};
|
||||
@@ -568,9 +574,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
const composable = useRunWorkflow({ router });
|
||||
const triggerNode = 'Chat Trigger';
|
||||
const nodeData = mock<ITaskData>();
|
||||
vi.mocked(mockDocumentStore.getChildNodes).mockReturnValue([
|
||||
{ name: 'Child node', type: 'nodes.child' },
|
||||
]);
|
||||
vi.mocked(mockDocumentStore.getChildNodes).mockReturnValue(['Child node']);
|
||||
mockDocumentStore.serialize.mockReturnValue(mock<WorkflowData>({ nodes: [] }));
|
||||
|
||||
const { runWorkflow } = composable;
|
||||
@@ -587,10 +591,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
},
|
||||
startNodes: [
|
||||
{
|
||||
name: {
|
||||
name: 'Child node',
|
||||
type: 'nodes.child',
|
||||
},
|
||||
name: 'Child node',
|
||||
sourceData: null,
|
||||
},
|
||||
],
|
||||
@@ -670,7 +671,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
};
|
||||
|
||||
vi.mocked(mockDocumentStore.getNodeByName).mockImplementation((name: string) =>
|
||||
name === 'Test node' ? { id: 'Test id', name: 'Test node' } : undefined,
|
||||
name === 'Test node' ? createTestNode({ id: 'Test id', name: 'Test node' }) : null,
|
||||
);
|
||||
|
||||
vi.mocked(pushConnectionStore).isConnected = true;
|
||||
@@ -1280,7 +1281,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
if (name === topNode) return getNodeUi(topNode, [100, 50]);
|
||||
if (name === middleNode) return getNodeUi(middleNode, [200, 200]);
|
||||
if (name === bottomNode) return getNodeUi(bottomNode, [150, 350]);
|
||||
return undefined;
|
||||
return null;
|
||||
});
|
||||
|
||||
// Test with different order of input nodes
|
||||
|
||||
@@ -189,7 +189,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
||||
directParentNodes,
|
||||
runData,
|
||||
workflowData.pinData,
|
||||
workflowDocumentStore.value.getSnapshot(),
|
||||
workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const { startNodeNames } = consolidatedData;
|
||||
@@ -539,7 +539,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
|
||||
// execution finished before it could be stopped
|
||||
const executedData = {
|
||||
data: execution.data,
|
||||
workflowData: workflowsStore.workflow,
|
||||
workflowData: workflowDocumentStore.value.getSnapshot(),
|
||||
finished: execution.finished,
|
||||
mode: execution.mode,
|
||||
startedAt: execution.startedAt,
|
||||
|
||||
@@ -407,7 +407,7 @@ export function useWorkflowExtraction() {
|
||||
);
|
||||
|
||||
for (const node of selectionChildNodes) {
|
||||
const currentNode = workflowsStore.workflow.nodes.find((x) => x.id === node.id);
|
||||
const currentNode = workflowDocumentStore?.value?.allNodes.find((x) => x.id === node.id);
|
||||
|
||||
if (isEqual(node, currentNode)) continue;
|
||||
|
||||
@@ -461,7 +461,7 @@ export function useWorkflowExtraction() {
|
||||
) {
|
||||
const { start, end } = selection;
|
||||
|
||||
const allNodeNames = workflowsStore.workflow.nodes.map((x) => x.name);
|
||||
const allNodeNames = workflowDocumentStore?.value?.allNodes.map((x) => x.name) ?? [];
|
||||
|
||||
let startNodeName = 'Start';
|
||||
const subGraphNames = subGraph.map((x) => x.name);
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function resolveParameter<T = IDataObject>(
|
||||
|
||||
return await resolveParameterImpl(
|
||||
parameter,
|
||||
workflowDocumentStore.getSnapshot(),
|
||||
workflowDocumentStore.getWorkflowObjectAccessorSnapshot(),
|
||||
workflowDocumentStore.connectionsBySourceNode,
|
||||
useEnvironmentsStore().variablesAsObject,
|
||||
useNDVStore().activeNode,
|
||||
|
||||
@@ -84,9 +84,7 @@ export function useWorkflowState() {
|
||||
setActiveExecutionId(undefined);
|
||||
workflowStateStore.executingNode.clearNodeExecutionQueue();
|
||||
ws.executionWaitingForWebhook = false;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(ws.workflow.id),
|
||||
);
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(ws.workflowId));
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore.name, 'IDLE');
|
||||
ws.workflowExecutionStartedData = undefined;
|
||||
|
||||
|
||||
@@ -35,9 +35,11 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { serializeNode } from '@/app/utils/nodes/nodeTransforms';
|
||||
import type { WorkflowObjectAccessors } from '../types';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import type { INode, IPinData } from 'n8n-workflow';
|
||||
import type { INode, IPinData, ProjectSharingData } from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { IUsedCredential } from '@/features/credentials/credentials.types';
|
||||
|
||||
export {
|
||||
getPinDataSize,
|
||||
@@ -318,7 +320,10 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
||||
});
|
||||
}
|
||||
|
||||
function getSnapshot(): WorkflowObjectAccessors {
|
||||
/**
|
||||
* @deprecated use individual method or `getSnapshot()`
|
||||
*/
|
||||
function getWorkflowObjectAccessorSnapshot(): WorkflowObjectAccessors {
|
||||
return {
|
||||
id: workflowId,
|
||||
connectionsBySourceNode: workflowDocumentConnections.connectionsBySourceNode.value,
|
||||
@@ -333,6 +338,35 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot(): IWorkflowDb {
|
||||
return {
|
||||
id: workflowId,
|
||||
name: workflowDocumentName.name.value,
|
||||
description: workflowDocumentDescription.description.value,
|
||||
active: workflowDocumentActive.active.value,
|
||||
activeVersionId: workflowDocumentActive.activeVersionId.value,
|
||||
isArchived: workflowDocumentIsArchived.isArchived.value,
|
||||
createdAt: workflowDocumentTimestamps.createdAt.value,
|
||||
updatedAt: workflowDocumentTimestamps.updatedAt.value,
|
||||
nodes: workflowDocumentNodes.allNodes.value,
|
||||
connections: workflowDocumentConnections.connectionsBySourceNode.value,
|
||||
settings: { ...DEFAULT_SETTINGS, ...workflowDocumentSettings.settings.value },
|
||||
tags: [...workflowDocumentTags.tags.value],
|
||||
pinData: workflowDocumentPinData.pinData.value as IPinData,
|
||||
sharedWithProjects: (workflowDocumentSharedWithProjects.sharedWithProjects.value ??
|
||||
[]) as ProjectSharingData[],
|
||||
homeProject: workflowDocumentHomeProject.homeProject.value ?? undefined,
|
||||
scopes: workflowDocumentScopes.scopes.value as Scope[],
|
||||
versionId: workflowDocumentVersionData.versionId.value,
|
||||
usedCredentials: Object.values(
|
||||
workflowDocumentUsedCredentials.usedCredentials.value,
|
||||
) as IUsedCredential[],
|
||||
meta: workflowDocumentMeta.meta.value,
|
||||
parentFolder: workflowDocumentParentFolder.parentFolder.value ?? undefined,
|
||||
checksum: workflowDocumentChecksum.checksum.value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
workflowVersion,
|
||||
@@ -363,6 +397,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
||||
hydrate,
|
||||
reset,
|
||||
getSnapshot,
|
||||
getWorkflowObjectAccessorSnapshot,
|
||||
serialize,
|
||||
cloneWorkflowObject,
|
||||
createWorkflowObject,
|
||||
|
||||
@@ -1044,6 +1044,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
);
|
||||
|
||||
return {
|
||||
/**
|
||||
* @deprecated use granular methods or getSnapshot() in workflow document store.
|
||||
*/
|
||||
workflow,
|
||||
currentWorkflowExecutions,
|
||||
workflowExecutionData,
|
||||
|
||||
@@ -246,8 +246,6 @@ const {
|
||||
fetchWorkflowDataFromUrl,
|
||||
resetWorkspace,
|
||||
initializeWorkspace,
|
||||
editableWorkflow,
|
||||
editableWorkflowObject,
|
||||
lastClickPosition,
|
||||
startChat,
|
||||
addNodesAndConnections,
|
||||
@@ -397,7 +395,11 @@ async function openWorkflow(data: IWorkflowDb) {
|
||||
*/
|
||||
|
||||
const triggerNodes = computed(() => {
|
||||
return editableWorkflow.value.nodes.filter((node) => nodeTypesStore.isTriggerNode(node.type));
|
||||
return (
|
||||
workflowDocumentStore?.value?.allNodes.filter((node) =>
|
||||
nodeTypesStore.isTriggerNode(node.type),
|
||||
) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const containsTriggerNodes = computed(() => triggerNodes.value.length > 0);
|
||||
@@ -1209,13 +1211,15 @@ function onRunWorkflowButtonMouseLeave() {
|
||||
*/
|
||||
|
||||
const chatTriggerNode = computed(() => {
|
||||
return editableWorkflow.value.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||
return workflowDocumentStore?.value?.allNodes.find(
|
||||
(node) => node.type === CHAT_TRIGGER_NODE_TYPE,
|
||||
);
|
||||
});
|
||||
|
||||
const containsChatTriggerNodes = computed(() => {
|
||||
return (
|
||||
!isExecutionWaitingForWebhook.value &&
|
||||
!!editableWorkflow.value.nodes.find(
|
||||
!!workflowDocumentStore?.value?.allNodes.find(
|
||||
(node) =>
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||
node.disabled !== true,
|
||||
@@ -1277,7 +1281,9 @@ function onToggleChat() {
|
||||
* Evaluation
|
||||
*/
|
||||
const evaluationTriggerNode = computed(() => {
|
||||
return editableWorkflow.value.nodes.find((node) => node.type === EVALUATION_TRIGGER_NODE_TYPE);
|
||||
return workflowDocumentStore?.value?.allNodes.find(
|
||||
(node) => node.type === EVALUATION_TRIGGER_NODE_TYPE,
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1572,7 +1578,11 @@ watch([() => route.name, () => route.params.workflowId], () => {
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return isLoading.value || isCanvasReadOnly.value || editableWorkflow.value.nodes.length !== 0;
|
||||
return (
|
||||
isLoading.value ||
|
||||
isCanvasReadOnly.value ||
|
||||
(workflowDocumentStore?.value?.allNodes ?? []).length !== 0
|
||||
);
|
||||
},
|
||||
(isReadOnlyOrLoading) => {
|
||||
if (isReadOnlyOrLoading) {
|
||||
@@ -1785,11 +1795,9 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div :class="$style.wrapper">
|
||||
<WorkflowCanvas
|
||||
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
||||
:id="editableWorkflow.id"
|
||||
v-if="!isLoading"
|
||||
:id="workflowDocumentStore?.workflowId"
|
||||
ref="canvas"
|
||||
:workflow="editableWorkflow"
|
||||
:workflow-object="editableWorkflowObject"
|
||||
:fallback-nodes="fallbackNodes"
|
||||
:show-fallback-nodes="showFallbackNodes"
|
||||
:event-bus="canvasEventBus"
|
||||
@@ -1935,7 +1943,6 @@ onBeforeUnmount(() => {
|
||||
<Suspense>
|
||||
<LazyNodeDetailsView
|
||||
v-if="!isNDVV2"
|
||||
:workflow-object="editableWorkflowObject"
|
||||
:read-only="isCanvasReadOnly"
|
||||
:is-production-execution-preview="nodeHelpers.isProductionExecutionPreview.value"
|
||||
:renaming="false"
|
||||
@@ -1948,7 +1955,6 @@ onBeforeUnmount(() => {
|
||||
<Suspense>
|
||||
<LazyNodeDetailsViewV2
|
||||
v-if="isNDVV2"
|
||||
:workflow-object="editableWorkflowObject"
|
||||
:read-only="isCanvasReadOnly"
|
||||
:is-production-execution-preview="nodeHelpers.isProductionExecutionPreview.value"
|
||||
@rename-node="onRenameNode"
|
||||
|
||||
@@ -32,6 +32,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
|
||||
settings: {},
|
||||
getPinDataSnapshot: vi.fn().mockReturnValue({}),
|
||||
getNodeByName: vi.fn().mockReturnValue(null),
|
||||
getSnapshot: vi.fn().mockReturnValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -388,9 +388,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
}
|
||||
: undefined,
|
||||
currentWorkflow: workflowDataStale.value
|
||||
? await assistantHelpers.simplifyWorkflowForAssistant(workflowsStore.workflow, {
|
||||
excludeParameterValues: !allowSendingParameterValues.value,
|
||||
})
|
||||
? await assistantHelpers.simplifyWorkflowForAssistant(
|
||||
workflowDocumentStore.value.getSnapshot(),
|
||||
{
|
||||
excludeParameterValues: !allowSendingParameterValues.value,
|
||||
},
|
||||
)
|
||||
: undefined,
|
||||
executionData:
|
||||
workflowExecutionDataStale.value && executionResult
|
||||
|
||||
@@ -1004,7 +1004,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
||||
const payload = await createBuilderPayload(text, userMessageId, {
|
||||
workflowId: workflowsStore.workflowId,
|
||||
quickReplyType,
|
||||
workflow: workflowsStore.workflow,
|
||||
workflow: workflowDocumentStore.value.getSnapshot(),
|
||||
executionData: executionResult,
|
||||
nodesForSchema: Object.keys(workflowDocumentStore.value.nodesByName),
|
||||
mode: modeForPayload,
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export function useBuilderExecution(isReady: ComputedRef<boolean>) {
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
const triggerNodes = computed(() =>
|
||||
workflowsStore.workflow.nodes.filter((node) => nodeTypesStore.isTriggerNode(node.type)),
|
||||
workflowDocumentStore.value.allNodes.filter((node) => nodeTypesStore.isTriggerNode(node.type)),
|
||||
);
|
||||
|
||||
// Empty until ready — prevents trigger selection in the execute button while setup is pending
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ const mockWorkflowDocumentStore = reactive({
|
||||
get allNodes() {
|
||||
return mockWorkflowsStore.workflow.nodes;
|
||||
},
|
||||
getSnapshot: vi.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
vi.mock('@/features/ai/assistant/builder.store', () => ({
|
||||
|
||||
+2
-2
@@ -351,12 +351,12 @@ export function useReviewChanges() {
|
||||
}
|
||||
|
||||
const sourceWorkflow = {
|
||||
...workflowsStore.workflow,
|
||||
...workflowDocumentStore.value.getSnapshot(),
|
||||
nodes: sourceNodes,
|
||||
connections: sourceConnections,
|
||||
};
|
||||
const targetWorkflow = {
|
||||
...workflowsStore.workflow,
|
||||
...workflowDocumentStore.value.getSnapshot(),
|
||||
nodes: targetCached.nodes,
|
||||
connections: targetCached.connections,
|
||||
};
|
||||
|
||||
@@ -563,6 +563,9 @@ export const useChatStore = defineStore(STORES.CHAT_HUB, () => {
|
||||
*/
|
||||
function initManualExecutionScaffold() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
@@ -576,7 +579,7 @@ export const useChatStore = defineStore(STORES.CHAT_HUB, () => {
|
||||
data: createRunExecutionData({
|
||||
resultData: { runData: {} },
|
||||
}),
|
||||
workflowData: workflowsStore.workflow,
|
||||
workflowData: workflowDocumentStore.getSnapshot(),
|
||||
};
|
||||
|
||||
// Signal canvas that an execution is pending (null = waiting for execution ID)
|
||||
|
||||
+1
-1
@@ -402,7 +402,7 @@ onMounted(async () => {
|
||||
node.parameters = resolved ?? {};
|
||||
}
|
||||
|
||||
previousWorkflow = { ...workflowsStore.workflow };
|
||||
previousWorkflow = { ...workflowDocumentStore.value.getSnapshot() };
|
||||
const targetDocStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowData.id));
|
||||
targetDocStore.hydrate(workflowData);
|
||||
} catch (error) {
|
||||
|
||||
+10
-2
@@ -16,9 +16,16 @@ import type { ExecutionSummary } from 'n8n-workflow';
|
||||
import { useDebounce } from '@/app/composables/useDebounce';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { executionRetryMessage } from '../executions.utils';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
const executionsStore = useExecutionsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -149,12 +156,13 @@ async function initializeRoute() {
|
||||
function fetchWorkflow() {
|
||||
// Skip fetching if it's a new workflow that hasn't been saved yet
|
||||
if (isNewWorkflowRoute.value || !workflowId.value) {
|
||||
workflow.value = workflowsStore.workflow;
|
||||
workflow.value = workflowDocumentStore.value.getSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the workflow from the list store (already loaded by WorkflowLayout)
|
||||
workflow.value = workflowsListStore.workflowsById[workflowId.value] ?? workflowsStore.workflow;
|
||||
workflow.value =
|
||||
workflowsListStore.workflowsById[workflowId.value] ?? workflowDocumentStore.value.getSnapshot();
|
||||
}
|
||||
|
||||
async function onAutoRefreshToggle(value: boolean) {
|
||||
|
||||
+6
-5
@@ -16,12 +16,13 @@ import {
|
||||
import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
|
||||
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import { createLogTree, flattenLogEntries } from '../logs.utils';
|
||||
import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
const { mockDocumentStore } = vi.hoisted(() => ({
|
||||
mockDocumentStore: {
|
||||
workflowId: 'test-workflow-id',
|
||||
name: 'Test Workflow',
|
||||
allNodes: [] as unknown[],
|
||||
allNodes: [],
|
||||
getNodeByName: vi.fn(),
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
getChildNodes: vi.fn().mockReturnValue([]),
|
||||
@@ -29,7 +30,7 @@ const { mockDocumentStore } = vi.hoisted(() => ({
|
||||
checkIfNodeHasChatParent: vi.fn().mockReturnValue(false),
|
||||
checkIfToolNodeHasChatParent: vi.fn().mockReturnValue(false),
|
||||
getExpressionHandler: vi.fn().mockReturnValue(null),
|
||||
getSnapshot: vi.fn().mockReturnValue({
|
||||
getWorkflowObjectAccessorSnapshot: vi.fn().mockReturnValue({
|
||||
id: 'test-workflow-id',
|
||||
connectionsBySourceNode: {},
|
||||
pinData: {},
|
||||
@@ -41,8 +42,8 @@ const { mockDocumentStore } = vi.hoisted(() => ({
|
||||
getChildNodes: vi.fn().mockReturnValue([]),
|
||||
getParentNodesByDepth: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
connectionsBySourceNode: {} as Record<string, unknown>,
|
||||
pinData: {} as Record<string, unknown>,
|
||||
connectionsBySourceNode: {},
|
||||
pinData: {},
|
||||
incomingConnectionsByNodeName: vi.fn().mockReturnValue({}),
|
||||
outgoingConnectionsByNodeName: vi.fn().mockReturnValue({}),
|
||||
settings: {},
|
||||
@@ -59,7 +60,7 @@ const { mockDocumentStore } = vi.hoisted(() => ({
|
||||
versionId: '',
|
||||
meta: {},
|
||||
}),
|
||||
},
|
||||
} satisfies Partial<ReturnType<typeof useWorkflowDocumentStore>>,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({
|
||||
|
||||
+8
-3
@@ -1,22 +1,27 @@
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { computed } from 'vue';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export function useClearExecutionButtonVisible() {
|
||||
const route = useRoute();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
|
||||
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
|
||||
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
|
||||
const { editableWorkflow } = useCanvasOperations();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const isReadOnlyEnvironment = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
const allTriggerNodesDisabled = computed(() =>
|
||||
editableWorkflow.value.nodes
|
||||
workflowDocumentStore.value.allNodes
|
||||
.filter((node) => nodeTypesStore.isTriggerNode(node.type))
|
||||
.every((node) => node.disabled),
|
||||
);
|
||||
|
||||
+2
-6
@@ -157,9 +157,7 @@ describe('SettingsSourceControl', () => {
|
||||
|
||||
it('should show SSH-specific fields when SSH protocol is selected', async () => {
|
||||
await nextTick();
|
||||
const { container, getByTestId } = renderComponent({
|
||||
pinia,
|
||||
});
|
||||
const { container, getByTestId } = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => expect(sourceControlStore.preferences.publicKey).not.toEqual(''));
|
||||
|
||||
@@ -179,9 +177,7 @@ describe('SettingsSourceControl', () => {
|
||||
|
||||
it('should show HTTPS-specific fields when HTTPS protocol is selected', async () => {
|
||||
await nextTick();
|
||||
const { container, queryByTestId } = renderComponent({
|
||||
pinia,
|
||||
});
|
||||
const { container, queryByTestId } = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => expect(sourceControlStore.preferences.publicKey).not.toEqual(''));
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ const nodeData = computed(
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const workflowObjectAccessors = computed(() =>
|
||||
workflowDocumentStore?.value ? workflowDocumentStore.value.getSnapshot() : undefined,
|
||||
workflowDocumentStore?.value
|
||||
? workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot()
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const nodeInputIssues = computed(() => {
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ const emit = defineEmits<{
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflow.id)),
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
@@ -289,6 +289,8 @@ describe('VirtualSchema.vue', () => {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflow.id),
|
||||
);
|
||||
workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null });
|
||||
workflowDocumentStore.setName(workflowsStore.workflow.name);
|
||||
|
||||
renderComponent = createComponentRenderer(VirtualSchema, {
|
||||
global: {
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useCalloutHelpers } from '@/app/composables/useCalloutHelpers';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { executionDataToJson } from '@/app/utils/nodeTypesUtils';
|
||||
import {
|
||||
@@ -46,7 +45,6 @@ import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { isEmpty } from '@/app/utils/typesUtils';
|
||||
import { asyncComputed } from '@vueuse/core';
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||
import pick from 'lodash/pick';
|
||||
import { DateTime } from 'luxon';
|
||||
import NodeExecuteButton from '@/app/components/NodeExecuteButton.vue';
|
||||
import { I18nT } from 'vue-i18n';
|
||||
@@ -87,7 +85,6 @@ const telemetryContext = useTelemetryContext();
|
||||
const i18n = useI18n();
|
||||
const ndvStore = useNDVStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const schemaPreviewStore = useSchemaPreviewStore();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
@@ -221,7 +218,11 @@ const contextSchema = computed(() => {
|
||||
mode: 'test',
|
||||
resumeUrl: i18n.baseText('dataMapping.schemaView.execution.resumeUrl'),
|
||||
},
|
||||
$workflow: pick(workflowsStore.workflow, ['id', 'name', 'active']),
|
||||
$workflow: {
|
||||
id: workflowDocumentStore?.value?.workflowId ?? '',
|
||||
name: workflowDocumentStore?.value?.name ?? '',
|
||||
active: workflowDocumentStore?.value?.active ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
return filterSchema(getSchema(schemaSource), props.search);
|
||||
|
||||
@@ -15,14 +15,9 @@ import {
|
||||
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import {
|
||||
createTestWorkflow,
|
||||
createTestWorkflowObject,
|
||||
defaultNodeDescriptions,
|
||||
mockNodes,
|
||||
} from '@/__tests__/mocks';
|
||||
import { computed } from 'vue';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { createTestWorkflow, defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
return {
|
||||
@@ -50,8 +45,13 @@ async function createPiniaStore(isActiveNode: boolean) {
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
workflowsStore.workflow = workflow;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
|
||||
workflowDocumentStore.setNodes(workflow.nodes);
|
||||
workflowDocumentStore.setConnections(workflow.connections);
|
||||
workflowDocumentStore.setSettings(workflow.settings ?? { executionOrder: 'v1' });
|
||||
workflowDocumentStore.initPristineNodeMetadata(node.name);
|
||||
|
||||
const workflowDocumentStoreRef = shallowRef(workflowDocumentStore);
|
||||
|
||||
if (isActiveNode) {
|
||||
ndvStore.setActiveNodeName(node.name, 'other');
|
||||
}
|
||||
@@ -62,7 +62,7 @@ async function createPiniaStore(isActiveNode: boolean) {
|
||||
return {
|
||||
pinia,
|
||||
workflow,
|
||||
workflowObject: createTestWorkflowObject(workflow),
|
||||
workflowDocumentStoreRef,
|
||||
nodeName: node.name,
|
||||
};
|
||||
}
|
||||
@@ -83,15 +83,13 @@ describe('NodeDetailsView', () => {
|
||||
});
|
||||
|
||||
it('should render correctly', async () => {
|
||||
const { pinia, workflow, workflowObject } = await createPiniaStore(true);
|
||||
const { pinia, workflow, workflowDocumentStoreRef } = await createPiniaStore(true);
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
||||
props: {
|
||||
workflowObject,
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
[WorkflowIdKey as unknown as string]: computed(() => workflow.id),
|
||||
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
|
||||
},
|
||||
mocks: {
|
||||
$route: {
|
||||
@@ -110,15 +108,13 @@ describe('NodeDetailsView', () => {
|
||||
|
||||
describe('keyboard listener', () => {
|
||||
test('should register and unregister keydown listener based on modal open state', async () => {
|
||||
const { pinia, workflow, workflowObject } = await createPiniaStore(true);
|
||||
const { pinia, workflow, workflowDocumentStoreRef } = await createPiniaStore(true);
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
||||
props: {
|
||||
workflowObject,
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
[WorkflowIdKey as unknown as string]: computed(() => workflow.id),
|
||||
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
|
||||
},
|
||||
mocks: {
|
||||
$route: {
|
||||
@@ -128,9 +124,7 @@ describe('NodeDetailsView', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId, unmount } = renderComponent({
|
||||
pinia,
|
||||
});
|
||||
const { getByTestId, queryByTestId, unmount } = renderComponent({ pinia });
|
||||
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
||||
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
||||
@@ -154,16 +148,14 @@ describe('NodeDetailsView', () => {
|
||||
});
|
||||
|
||||
test('should unregister keydown listener on unmount', async () => {
|
||||
const { pinia, workflow, workflowObject, nodeName } = await createPiniaStore(false);
|
||||
const { pinia, workflow, workflowDocumentStoreRef, nodeName } = await createPiniaStore(false);
|
||||
const ndvStore = useNDVStore(pinia);
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
||||
props: {
|
||||
workflowObject,
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
[WorkflowIdKey as unknown as string]: computed(() => workflow.id),
|
||||
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
|
||||
},
|
||||
mocks: {
|
||||
$route: {
|
||||
@@ -173,9 +165,7 @@ describe('NodeDetailsView', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId, unmount } = renderComponent({
|
||||
pinia,
|
||||
});
|
||||
const { getByTestId, queryByTestId, unmount } = renderComponent({ pinia });
|
||||
|
||||
ndvStore.setActiveNodeName(nodeName, 'other');
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { IRunData, NodeConnectionType, IConnectedNode } from 'n8n-workflow'
|
||||
import { jsonParse, NodeHelpers, NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type { IRunDataDisplayMode, IUpdateInformation, TargetItem } from '@/Interface';
|
||||
import type { NodePanelType } from '@/features/ndv/shared/ndv.types';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types/workflow';
|
||||
|
||||
import NodeSettings from '@/features/ndv/settings/components/NodeSettings.vue';
|
||||
import NDVDraggablePanels from '../../panel/components/NDVDraggablePanels.vue';
|
||||
@@ -54,7 +53,6 @@ const emit = defineEmits<{
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
workflowObject: WorkflowObjectAccessors;
|
||||
readOnly?: boolean;
|
||||
renaming?: boolean;
|
||||
isProductionExecutionPreview?: boolean;
|
||||
@@ -100,6 +98,10 @@ const isPairedItemHoveringEnabled = ref(true);
|
||||
|
||||
const pushRef = computed(() => ndvStore.pushRef);
|
||||
|
||||
const workflowObject = computed(() =>
|
||||
workflowDocumentStore?.value?.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const activeNodeType = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return nodeTypesStore.getNodeType(activeNode.value.type, activeNode.value.typeVersion);
|
||||
@@ -132,7 +134,7 @@ const workflowRunData = computed(() => {
|
||||
|
||||
const parentNodes = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return props.workflowObject.getParentNodesByDepth(activeNode.value.name, 1);
|
||||
return workflowObject.value?.getParentNodesByDepth(activeNode.value.name, 1) ?? [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
@@ -152,8 +154,8 @@ const parentNode = computed<IConnectedNode | undefined>(() => {
|
||||
|
||||
const inputNodeName = computed<string | undefined>(() => {
|
||||
const nodeOutputs =
|
||||
activeNode.value && activeNodeType.value
|
||||
? NodeHelpers.getNodeOutputs(props.workflowObject, activeNode.value, activeNodeType.value)
|
||||
activeNode.value && activeNodeType.value && workflowObject.value
|
||||
? NodeHelpers.getNodeOutputs(workflowObject.value, activeNode.value, activeNodeType.value)
|
||||
: [];
|
||||
|
||||
const nonMainOutputs = nodeOutputs.filter((output) => {
|
||||
@@ -168,7 +170,7 @@ const inputNodeName = computed<string | undefined>(() => {
|
||||
// For sub-nodes, we need to get their connected output node to determine the input
|
||||
// because sub-nodes use specialized outputs (e.g. NodeConnectionTypes.AiTool)
|
||||
// instead of the standard Main output type
|
||||
const connectedOutputNode = props.workflowObject.getChildNodes(
|
||||
const connectedOutputNode = workflowObject.value?.getChildNodes(
|
||||
activeNode.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
)?.[0];
|
||||
@@ -245,14 +247,14 @@ const maxInputRun = computed(() => {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const workflowNode = props.workflowObject.getNode(activeNode.value.name);
|
||||
const workflowNode = workflowObject.value?.getNode(activeNode.value.name);
|
||||
|
||||
if (!workflowNode || !activeNodeType.value) {
|
||||
if (!workflowNode || !activeNodeType.value || !workflowObject.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const outputs = NodeHelpers.getNodeOutputs(
|
||||
props.workflowObject,
|
||||
workflowObject.value,
|
||||
workflowNode,
|
||||
activeNodeType.value,
|
||||
);
|
||||
@@ -605,12 +607,12 @@ watch(
|
||||
|
||||
setTimeout(() => ndvStore.setNDVPushRef(), 0);
|
||||
|
||||
if (!activeNodeType.value) {
|
||||
if (!activeNodeType.value || !workflowObject.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
void externalHooks.run('dataDisplay.nodeTypeChanged', {
|
||||
nodeSubtitle: nodeHelpers.getNodeSubtitle(node, activeNodeType.value, props.workflowObject),
|
||||
nodeSubtitle: nodeHelpers.getNodeSubtitle(node, activeNodeType.value, workflowObject.value),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -744,7 +746,7 @@ onBeforeUnmount(() => {
|
||||
@activate="onWorkflowActivate"
|
||||
/>
|
||||
<InputPanel
|
||||
v-else-if="!isTriggerNode"
|
||||
v-else-if="!isTriggerNode && workflowObject"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
@@ -773,6 +775,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template #output>
|
||||
<OutputPanel
|
||||
v-if="workflowObject"
|
||||
data-test-id="output-panel"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
|
||||
+13
-16
@@ -14,15 +14,9 @@ import {
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import {
|
||||
createTestNode,
|
||||
createTestWorkflow,
|
||||
createTestWorkflowObject,
|
||||
defaultNodeDescriptions,
|
||||
} from '@/__tests__/mocks';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { createTestNode, createTestWorkflow, defaultNodeDescriptions } from '@/__tests__/mocks';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({}),
|
||||
@@ -48,21 +42,24 @@ const setupStore = (nodes: Array<ReturnType<typeof createTestNode>>) => {
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
workflowsStore.workflow = workflow;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
|
||||
workflowDocumentStore.hydrate(workflow);
|
||||
workflowDocumentStore.setAllNodeMetadata(
|
||||
nodes.reduce((acc, node) => ({ ...acc, [node.name]: { pristine: true } }), {}),
|
||||
);
|
||||
|
||||
const workflowDocumentStoreRef = shallowRef(workflowDocumentStore);
|
||||
|
||||
return {
|
||||
pinia,
|
||||
workflow,
|
||||
workflowObject: createTestWorkflowObject(workflow),
|
||||
workflowDocumentStoreRef,
|
||||
};
|
||||
};
|
||||
|
||||
describe('NodeDetailsViewV2', () => {
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let workflowId: string;
|
||||
let workflowObject: Workflow;
|
||||
let workflowDocumentStoreRef: ReturnType<typeof setupStore>['workflowDocumentStoreRef'];
|
||||
const manualTriggerNode = createTestNode({
|
||||
name: 'Manual Trigger',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
@@ -82,12 +79,12 @@ describe('NodeDetailsViewV2', () => {
|
||||
|
||||
const render = createComponentRenderer(NodeDetailsViewV2, {
|
||||
props: {
|
||||
workflowObject,
|
||||
...componentProps,
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
[WorkflowIdKey as unknown as string]: computed(() => workflowId),
|
||||
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
|
||||
},
|
||||
mocks: {
|
||||
$route: {
|
||||
@@ -123,7 +120,7 @@ describe('NodeDetailsViewV2', () => {
|
||||
const store = setupStore([manualTriggerNode, setNode, stickyNode]);
|
||||
pinia = store.pinia;
|
||||
workflowId = store.workflow.id;
|
||||
workflowObject = store.workflowObject;
|
||||
workflowDocumentStoreRef = store.workflowDocumentStoreRef;
|
||||
});
|
||||
|
||||
test('should not render when no node is active', () => {
|
||||
@@ -167,7 +164,7 @@ describe('NodeDetailsViewV2', () => {
|
||||
const store = setupStore([manualTriggerNode, setNode, stickyNode]);
|
||||
pinia = store.pinia;
|
||||
workflowId = store.workflow.id;
|
||||
workflowObject = store.workflowObject;
|
||||
workflowDocumentStoreRef = store.workflowDocumentStoreRef;
|
||||
});
|
||||
|
||||
test('should register keydown listener on mount', async () => {
|
||||
@@ -201,7 +198,7 @@ describe('NodeDetailsViewV2', () => {
|
||||
const store = setupStore([manualTriggerNode, setNode, stickyNode]);
|
||||
pinia = store.pinia;
|
||||
workflowId = store.workflow.id;
|
||||
workflowObject = store.workflowObject;
|
||||
workflowDocumentStoreRef = store.workflowDocumentStoreRef;
|
||||
});
|
||||
|
||||
test('should open dialog on mount', async () => {
|
||||
@@ -238,7 +235,7 @@ describe('NodeDetailsViewV2', () => {
|
||||
const store = setupStore([manualTriggerNode, setNode, stickyNode]);
|
||||
pinia = store.pinia;
|
||||
workflowId = store.workflow.id;
|
||||
workflowObject = store.workflowObject;
|
||||
workflowDocumentStoreRef = store.workflowDocumentStoreRef;
|
||||
});
|
||||
|
||||
test('should close NDV when close button is clicked', async () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { IRunDataDisplayMode, IUpdateInformation, TargetItem } from '@/Interface';
|
||||
import type { MainPanelType, NodePanelType } from '../ndv.types';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types/workflow';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { IRunData, NodeConnectionType } from 'n8n-workflow';
|
||||
import { jsonParse, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
|
||||
@@ -56,7 +55,6 @@ const emit = defineEmits<{
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
workflowObject: WorkflowObjectAccessors;
|
||||
readOnly?: boolean;
|
||||
isProductionExecutionPreview?: boolean;
|
||||
}>(),
|
||||
@@ -104,6 +102,10 @@ const mainPanelRef = useTemplateRef('mainPanelRef');
|
||||
// computed
|
||||
const pushRef = computed(() => ndvStore.pushRef);
|
||||
|
||||
const workflowObject = computed(() =>
|
||||
workflowDocumentStore?.value?.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const activeNodeType = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return nodeTypesStore.getNodeType(activeNode.value.type, activeNode.value.typeVersion);
|
||||
@@ -132,8 +134,8 @@ const workflowRunData = computed(() => {
|
||||
const parentNodes = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return (
|
||||
props.workflowObject
|
||||
.getParentNodesByDepth(activeNode.value.name, 1)
|
||||
workflowObject.value
|
||||
?.getParentNodesByDepth(activeNode.value.name, 1)
|
||||
.map(({ name }) => name) || []
|
||||
);
|
||||
} else {
|
||||
@@ -156,8 +158,8 @@ const parentNode = computed(() => {
|
||||
|
||||
const inputNodeName = computed<string | undefined>(() => {
|
||||
const nodeOutputs =
|
||||
activeNode.value && activeNodeType.value
|
||||
? NodeHelpers.getNodeOutputs(props.workflowObject, activeNode.value, activeNodeType.value)
|
||||
activeNode.value && activeNodeType.value && workflowObject.value
|
||||
? NodeHelpers.getNodeOutputs(workflowObject.value, activeNode.value, activeNodeType.value)
|
||||
: [];
|
||||
|
||||
const nonMainOutputs = nodeOutputs.filter((output) => {
|
||||
@@ -172,7 +174,7 @@ const inputNodeName = computed<string | undefined>(() => {
|
||||
// For sub-nodes, we need to get their connected output node to determine the input
|
||||
// because sub-nodes use specialized outputs (e.g. NodeConnectionTypes.AiTool)
|
||||
// instead of the standard Main output type
|
||||
const connectedOutputNode = props.workflowObject.getChildNodes(
|
||||
const connectedOutputNode = workflowObject.value?.getChildNodes(
|
||||
activeNode.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
)?.[0];
|
||||
@@ -249,14 +251,14 @@ const maxInputRun = computed(() => {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const workflowNode = props.workflowObject.getNode(activeNode.value.name);
|
||||
const workflowNode = workflowObject.value?.getNode(activeNode.value.name);
|
||||
|
||||
if (!workflowNode || !activeNodeType.value) {
|
||||
if (!workflowNode || !activeNodeType.value || !workflowObject.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const outputs = NodeHelpers.getNodeOutputs(
|
||||
props.workflowObject,
|
||||
workflowObject.value,
|
||||
workflowNode,
|
||||
activeNodeType.value,
|
||||
);
|
||||
@@ -599,12 +601,12 @@ watch(
|
||||
|
||||
setTimeout(() => ndvStore.setNDVPushRef(), 0);
|
||||
|
||||
if (!activeNodeType.value) {
|
||||
if (!activeNodeType.value || !workflowObject.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
void externalHooks.run('dataDisplay.nodeTypeChanged', {
|
||||
nodeSubtitle: nodeHelpers.getNodeSubtitle(node, activeNodeType.value, props.workflowObject),
|
||||
nodeSubtitle: nodeHelpers.getNodeSubtitle(node, activeNodeType.value, workflowObject.value),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -756,7 +758,7 @@ onBeforeUnmount(() => {
|
||||
@activate="onWorkflowActivate"
|
||||
/>
|
||||
<InputPanel
|
||||
v-else-if="!isTriggerNode"
|
||||
v-else-if="!isTriggerNode && workflowObject"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
@@ -827,6 +829,7 @@ onBeforeUnmount(() => {
|
||||
:style="{ width: `${panelWidthPercentage.right}%` }"
|
||||
>
|
||||
<OutputPanel
|
||||
v-if="workflowObject"
|
||||
data-test-id="output-panel"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
|
||||
+21
-27
@@ -14,23 +14,12 @@ import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
const mockEditableWorkflow = {
|
||||
value: {
|
||||
nodes: [] as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
}>,
|
||||
},
|
||||
};
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
|
||||
vi.mock('@/app/composables/useCanvasOperations', () => ({
|
||||
useCanvasOperations: () => ({
|
||||
addNodes: vi.fn(),
|
||||
setNodeActive: vi.fn(),
|
||||
editableWorkflow: mockEditableWorkflow,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -40,11 +29,6 @@ vi.mock('@/features/workflows/canvas/canvas.eventBus', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
injectWorkflowDocumentStore: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
const mockGenerateMergedNodesAndActionsFn = vi.fn().mockReturnValue({ mergedNodes: [] });
|
||||
|
||||
vi.mock('@/features/shared/nodeCreator/composables/useActionsGeneration', () => ({
|
||||
@@ -133,8 +117,6 @@ describe('useNodeCommands', () => {
|
||||
});
|
||||
|
||||
mockAddNodes.mockResolvedValue([{ id: 'node-1' }]);
|
||||
|
||||
mockEditableWorkflow.value.nodes = [];
|
||||
});
|
||||
|
||||
describe('add node command', () => {
|
||||
@@ -245,15 +227,21 @@ describe('useNodeCommands', () => {
|
||||
});
|
||||
|
||||
it('should populate open node children with workflow nodes', () => {
|
||||
mockEditableWorkflow.value.nodes = [
|
||||
{ id: 'node-1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1 },
|
||||
{
|
||||
const store = useWorkflowDocumentStore(createWorkflowDocumentId('123'));
|
||||
store.setNodes([
|
||||
createTestNode({
|
||||
id: 'node-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
}),
|
||||
createTestNode({
|
||||
id: 'node-2',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 1,
|
||||
},
|
||||
];
|
||||
}),
|
||||
]);
|
||||
|
||||
const { commands } = useNodeCommands({
|
||||
lastQuery: ref(''),
|
||||
@@ -320,9 +308,15 @@ describe('useNodeCommands', () => {
|
||||
|
||||
describe('root open node items', () => {
|
||||
beforeEach(() => {
|
||||
mockEditableWorkflow.value.nodes = [
|
||||
{ id: 'node-1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1 },
|
||||
];
|
||||
const store = useWorkflowDocumentStore(createWorkflowDocumentId('123'));
|
||||
store.setNodes([
|
||||
createTestNode({
|
||||
id: 'node-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not show root open node items when query is too short', () => {
|
||||
|
||||
+3
-3
@@ -33,7 +33,7 @@ export function useNodeCommands(options: {
|
||||
const i18n = useI18n();
|
||||
const { lastQuery } = options;
|
||||
|
||||
const { addNodes, setNodeActive, editableWorkflow } = useCanvasOperations();
|
||||
const { addNodes, setNodeActive } = useCanvasOperations();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
@@ -139,7 +139,7 @@ export function useNodeCommands(options: {
|
||||
};
|
||||
|
||||
const openNodeCommands = computed<CommandBarItem[]>(() => {
|
||||
return editableWorkflow.value.nodes.map((node) => buildOpenNodeCommand(node, false));
|
||||
return workflowDocumentStore.value.allNodes.map((node) => buildOpenNodeCommand(node, false));
|
||||
});
|
||||
|
||||
const rootOpenNodeCommandItems = computed<CommandBarItem[]>(() => {
|
||||
@@ -147,7 +147,7 @@ export function useNodeCommands(options: {
|
||||
return [];
|
||||
}
|
||||
|
||||
return editableWorkflow.value.nodes.map((node) => buildOpenNodeCommand(node, true));
|
||||
return workflowDocumentStore.value.allNodes.map((node) => buildOpenNodeCommand(node, true));
|
||||
});
|
||||
|
||||
const nodeCommands = computed<CommandBarItem[]>(() => {
|
||||
|
||||
+14
-11
@@ -1,7 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi, beforeEach, type MockInstance } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useWorkflowCommands } from './useWorkflowCommands';
|
||||
import * as useCanvasOperations from '@/app/composables/useCanvasOperations';
|
||||
import { useTagsStore } from '@/features/shared/tags/tags.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
@@ -21,7 +20,6 @@ import { shallowRef, type Ref } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
vi.mock('@/app/composables/useCanvasOperations');
|
||||
vi.mock('@/app/composables/useWorkflowHelpers');
|
||||
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
@@ -109,6 +107,9 @@ describe('useWorkflowCommands', () => {
|
||||
createWorkflowDocumentId(mockWorkflow.value.id),
|
||||
);
|
||||
mockWorkflowDocumentStore.setScopes(mockWorkflow.value.scopes ?? []);
|
||||
mockWorkflowDocumentStore.setName(mockWorkflow.value.name);
|
||||
mockWorkflowDocumentStore.setTags(mockWorkflow.value.tags ?? []);
|
||||
mockWorkflowDocumentStore.setNodes(mockWorkflow.value.nodes);
|
||||
vi.spyOn(mockWorkflowDocumentStore, 'serialize').mockReturnValue(
|
||||
mockWorkflow.value as unknown as WorkflowData,
|
||||
);
|
||||
@@ -133,9 +134,6 @@ describe('useWorkflowCommands', () => {
|
||||
|
||||
mockSourceControlStore.preferences.branchReadOnly = false;
|
||||
|
||||
const canvasOperationsMock: MockInstance = vi.spyOn(useCanvasOperations, 'useCanvasOperations');
|
||||
canvasOperationsMock.mockReturnValue({ editableWorkflow: mockWorkflow });
|
||||
|
||||
canvasEventBus.emit = vi.fn();
|
||||
nodeViewEventBus.emit = vi.fn();
|
||||
});
|
||||
@@ -149,7 +147,7 @@ describe('useWorkflowCommands', () => {
|
||||
});
|
||||
|
||||
it('should return credential commands when credentials exist', () => {
|
||||
mockWorkflow.value.nodes = [
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
name: 'node1',
|
||||
@@ -161,6 +159,8 @@ describe('useWorkflowCommands', () => {
|
||||
},
|
||||
} as unknown as INodeUi,
|
||||
];
|
||||
mockWorkflow.value.nodes = nodes;
|
||||
mockWorkflowDocumentStore.setNodes(nodes);
|
||||
|
||||
const { commands } = useWorkflowCommands();
|
||||
const credentialCommand = commands.value.find((cmd) => cmd.id === 'open-credential');
|
||||
@@ -171,7 +171,7 @@ describe('useWorkflowCommands', () => {
|
||||
});
|
||||
|
||||
it('should handle credential click', async () => {
|
||||
mockWorkflow.value.nodes = [
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
name: 'node1',
|
||||
@@ -183,6 +183,8 @@ describe('useWorkflowCommands', () => {
|
||||
},
|
||||
} as unknown as INodeUi,
|
||||
];
|
||||
mockWorkflow.value.nodes = nodes;
|
||||
mockWorkflowDocumentStore.setNodes(nodes);
|
||||
|
||||
const { commands } = useWorkflowCommands();
|
||||
const credentialCommand = commands.value.find((cmd) => cmd.id === 'open-credential');
|
||||
@@ -259,6 +261,7 @@ describe('useWorkflowCommands', () => {
|
||||
|
||||
it('should handle duplicate workflow', async () => {
|
||||
mockWorkflow.value.tags = ['tag1'];
|
||||
mockWorkflowDocumentStore.setTags(['tag1']);
|
||||
|
||||
const { commands } = useWorkflowCommands();
|
||||
const duplicateCommand = commands.value.find((cmd) => cmd.id === 'duplicate-workflow');
|
||||
@@ -323,8 +326,6 @@ describe('useWorkflowCommands', () => {
|
||||
|
||||
describe('subworkflow commands', () => {
|
||||
it('should return empty array when no subworkflows exist', () => {
|
||||
mockWorkflow.value.nodes = [];
|
||||
|
||||
const { commands } = useWorkflowCommands();
|
||||
const subworkflowCommand = commands.value.find((cmd) => cmd.id === 'open-sub-workflow');
|
||||
|
||||
@@ -332,7 +333,7 @@ describe('useWorkflowCommands', () => {
|
||||
});
|
||||
|
||||
it('should return subworkflow commands when Execute Workflow nodes exist', () => {
|
||||
mockWorkflow.value.nodes = [
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
name: 'node1',
|
||||
@@ -348,6 +349,8 @@ describe('useWorkflowCommands', () => {
|
||||
},
|
||||
} as unknown as INodeUi,
|
||||
];
|
||||
mockWorkflow.value.nodes = nodes;
|
||||
mockWorkflowDocumentStore.setNodes(nodes);
|
||||
|
||||
const { commands } = useWorkflowCommands();
|
||||
const subworkflowCommand = commands.value.find((cmd) => cmd.id === 'open-sub-workflow');
|
||||
|
||||
+7
-7
@@ -9,7 +9,6 @@ import { useTagsStore } from '@/features/shared/tags/tags.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
|
||||
import {
|
||||
@@ -54,7 +53,6 @@ const ITEM_ID = {
|
||||
|
||||
export function useWorkflowCommands(): CommandGroup {
|
||||
const i18n = useI18n();
|
||||
const { editableWorkflow } = useCanvasOperations();
|
||||
const rootStore = useRootStore();
|
||||
const uiStore = useUIStore();
|
||||
const tagsStore = useTagsStore();
|
||||
@@ -87,7 +85,9 @@ export function useWorkflowCommands(): CommandGroup {
|
||||
|
||||
const credentialCommands = computed<CommandBarItem[]>(() => {
|
||||
const credentials = uniqBy(
|
||||
editableWorkflow.value.nodes.map((node) => Object.values(node.credentials ?? {})).flat(),
|
||||
workflowDocumentStore.value.allNodes
|
||||
.map((node) => Object.values(node.credentials ?? {}))
|
||||
.flat(),
|
||||
(cred) => cred.id,
|
||||
);
|
||||
if (credentials.length === 0) {
|
||||
@@ -286,8 +286,8 @@ export function useWorkflowCommands(): CommandGroup {
|
||||
name: DUPLICATE_MODAL_KEY,
|
||||
data: {
|
||||
id: workflowsStore.workflowId,
|
||||
name: editableWorkflow.value.name,
|
||||
tags: editableWorkflow.value.tags,
|
||||
name: workflowDocumentStore.value.name,
|
||||
tags: workflowDocumentStore.value.tags,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -303,7 +303,7 @@ export function useWorkflowCommands(): CommandGroup {
|
||||
]);
|
||||
|
||||
const subworkflowCommands = computed<CommandBarItem[]>(() => {
|
||||
const subworkflows = editableWorkflow.value.nodes
|
||||
const subworkflows = workflowDocumentStore.value.allNodes
|
||||
.filter((node) => node.type === EXECUTE_WORKFLOW_NODE_TYPE)
|
||||
.map((node) => node?.parameters?.workflowId)
|
||||
.filter(
|
||||
@@ -366,7 +366,7 @@ export function useWorkflowCommands(): CommandGroup {
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
let name = editableWorkflow.value.name || 'unsaved_workflow';
|
||||
let name = workflowDocumentStore.value.name || 'unsaved_workflow';
|
||||
name = name.replace(/[^a-z0-9]/gi, '_');
|
||||
telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
||||
saveAs(blob, name + '.json');
|
||||
|
||||
+8
-2
@@ -248,8 +248,14 @@ export const hasActiveNode = (targetNodeParameterContext?: TargetNodeParameterCo
|
||||
return workflowDocumentStore.getNodeByName(targetNodeParameterContext.nodeName) !== null;
|
||||
};
|
||||
|
||||
export const isSplitInBatchesAbsent = () =>
|
||||
!useWorkflowsStore().workflow.nodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
||||
export const isSplitInBatchesAbsent = () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
return !workflowDocumentStore.allNodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
||||
};
|
||||
|
||||
export function autocompletableNodeNames(targetNodeParameterContext?: TargetNodeParameterContext) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
+25
-28
@@ -2,17 +2,16 @@ import { waitFor } from '@testing-library/vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import WorkflowCanvas from './WorkflowCanvas.vue';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { STICKY_NODE_TYPE } from '@/app/constants';
|
||||
import { CanvasNodeRenderType } from '../canvas.types';
|
||||
import {
|
||||
createTestNode,
|
||||
createTestWorkflow,
|
||||
createTestWorkflowObject,
|
||||
defaultNodeDescriptions,
|
||||
} from '@/__tests__/mocks';
|
||||
import { createTestNode, createTestWorkflow, defaultNodeDescriptions } from '@/__tests__/mocks';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import * as vueuse from '@vueuse/core';
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
@@ -23,16 +22,14 @@ vi.mock('@vueuse/core', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function setupWorkflow(workflow: IWorkflowDb) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
|
||||
workflowDocumentStore.hydrate(workflow);
|
||||
}
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowCanvas, {
|
||||
props: {
|
||||
id: 'canvas',
|
||||
workflow: createTestWorkflow({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
}),
|
||||
workflowObject: {} as Workflow,
|
||||
eventBus: createEventBus(),
|
||||
},
|
||||
});
|
||||
@@ -43,6 +40,15 @@ beforeEach(() => {
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
|
||||
setupWorkflow(
|
||||
createTestWorkflow({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -64,12 +70,9 @@ describe('WorkflowCanvas', () => {
|
||||
],
|
||||
connections: { 'Node 1': { main: [[{ node: 'Node 2', type: 'main', index: 0 }]] } },
|
||||
});
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow,
|
||||
workflowObject: createTestWorkflowObject(workflow),
|
||||
},
|
||||
});
|
||||
setupWorkflow(workflow);
|
||||
|
||||
const { container } = renderComponent();
|
||||
|
||||
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2));
|
||||
|
||||
@@ -103,13 +106,10 @@ describe('WorkflowCanvas', () => {
|
||||
nodes: [...stickyNodes],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
setupWorkflow(workflow);
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow,
|
||||
workflowObject,
|
||||
fallbackNodes,
|
||||
showFallbackNodes: true,
|
||||
},
|
||||
@@ -137,13 +137,10 @@ describe('WorkflowCanvas', () => {
|
||||
nodes,
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
setupWorkflow(workflow);
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
workflow,
|
||||
workflowObject,
|
||||
fallbackNodes,
|
||||
showFallbackNodes: false,
|
||||
},
|
||||
|
||||
+16
-10
@@ -6,11 +6,15 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { ViewportTransform } from '@vue-flow/core';
|
||||
import { getRectOfNodes, useVueFlow } from '@vue-flow/core';
|
||||
import { throttledRef } from '@vueuse/core';
|
||||
import { computed, ref, toRef, useCssModule, useTemplateRef } from 'vue';
|
||||
import { computed, ref, useCssModule, useTemplateRef } from 'vue';
|
||||
import type { CanvasEventBusEvents } from '../canvas.types';
|
||||
import { useCanvasMapping } from '../composables/useCanvasMapping';
|
||||
import Canvas from './Canvas.vue';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
@@ -19,8 +23,6 @@ defineOptions({
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
workflow: IWorkflowDb;
|
||||
workflowObject: WorkflowObjectAccessors;
|
||||
fallbackNodes?: IWorkflowDb['nodes'];
|
||||
showFallbackNodes?: boolean;
|
||||
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||
@@ -41,18 +43,23 @@ const props = withDefaults(
|
||||
|
||||
const canvasRef = useTemplateRef('canvas');
|
||||
const $style = useCssModule();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
|
||||
);
|
||||
|
||||
const { onNodesInitialized, viewport, viewportRef, getNodes, fitBounds } = useVueFlow(props.id);
|
||||
|
||||
const workflow = toRef(props, 'workflow');
|
||||
const workflowObject = toRef(props, 'workflowObject');
|
||||
const workflowObject = computed(() =>
|
||||
workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const nodes = computed(() => {
|
||||
return props.showFallbackNodes
|
||||
? [...props.workflow.nodes, ...props.fallbackNodes]
|
||||
: props.workflow.nodes;
|
||||
? [...workflowDocumentStore.value.allNodes, ...props.fallbackNodes]
|
||||
: workflowDocumentStore.value.allNodes;
|
||||
});
|
||||
const connections = computed(() => props.workflow.connections);
|
||||
const connections = computed(() => workflowDocumentStore.value.connectionsBySourceNode);
|
||||
|
||||
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
||||
nodes,
|
||||
@@ -148,7 +155,6 @@ defineExpose({
|
||||
<div :class="$style.wrapper" data-test-id="canvas-wrapper">
|
||||
<div id="canvas" :class="$style.canvas">
|
||||
<Canvas
|
||||
v-if="workflow"
|
||||
:id="id"
|
||||
ref="canvas"
|
||||
:nodes="executing ? mappedNodesThrottled : mappedNodes"
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export function useExpressionResolveCtx(node: ComputedRef<INodeUi | null | undef
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: environmentsStore.variablesAsObject,
|
||||
workflow: workflowDocumentStore.value.getSnapshot(),
|
||||
workflow: workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
execution,
|
||||
nodeName,
|
||||
additionalKeys: {},
|
||||
|
||||
Reference in New Issue
Block a user