chore: Move router usage out of useCanvasOperation and useWorkflowHelpers (no-changelog) (#16041)

This commit is contained in:
Charlie Kolb
2025-06-05 13:51:07 +02:00
committed by GitHub
parent 4a6bcffc70
commit 2724089078
37 changed files with 877 additions and 889 deletions

View File

@@ -5,7 +5,7 @@ import LogsPanel from '@/components/CanvasChat/future/LogsPanel.vue';
import { useSettingsStore } from '@/stores/settings.store';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { createRouter, createWebHistory, useRouter } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { h, nextTick } from 'vue';
import {
@@ -281,8 +281,7 @@ describe('LogsPanel', () => {
});
it('should still show logs for a removed node', async () => {
const router = useRouter();
const operations = useCanvasOperations({ router });
const operations = useCanvasOperations();
logsStore.toggleOpen(true);
workflowsStore.setWorkflow(deepCopy(aiChatWorkflow));

View File

@@ -13,6 +13,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
const props = defineProps<{
modalName: string;
@@ -27,7 +28,8 @@ const props = defineProps<{
}>();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowSaving = useWorkflowSaving({ router });
const workflowHelpers = useWorkflowHelpers();
const { showMessage, showError } = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();
@@ -103,7 +105,7 @@ const save = async (): Promise<void> => {
);
}
const saved = await workflowHelpers.saveAsNewWorkflow({
const saved = await workflowSaving.saveAsNewWorkflow({
name: workflowName,
data: workflowToUpdate,
tags: currentTagIds.value,

View File

@@ -59,6 +59,7 @@ import { useFoldersStore } from '@/stores/folders.store';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { type BaseTextKey, useI18n } from '@n8n/i18n';
import { ProjectTypes } from '@/types/projects.types';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
const props = defineProps<{
readOnly?: boolean;
@@ -95,7 +96,8 @@ const telemetry = useTelemetry();
const message = useMessage();
const toast = useToast();
const documentTitle = useDocumentTitle();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowSaving = useWorkflowSaving({ router });
const workflowHelpers = useWorkflowHelpers();
const pageRedirectionHelper = usePageRedirectionHelper();
const isTagsEditEnabled = ref(false);
@@ -290,7 +292,7 @@ async function onSaveButtonClick() {
const name = props.name;
const tags = props.tags as string[];
const saved = await workflowHelpers.saveCurrentWorkflow({
const saved = await workflowSaving.saveCurrentWorkflow({
id,
name,
tags,
@@ -347,7 +349,7 @@ async function onTagsBlur() {
}
tagsSaving.value = true;
const saved = await workflowHelpers.saveCurrentWorkflow({ tags });
const saved = await workflowSaving.saveCurrentWorkflow({ tags });
telemetry.track('User edited workflow tags', {
workflow_id: props.id,
new_tag_count: tags.length,
@@ -390,7 +392,7 @@ async function onNameSubmit(name: string) {
uiStore.addActiveAction('workflowSaving');
const id = getWorkflowId();
const saved = await workflowHelpers.saveCurrentWorkflow({ name });
const saved = await workflowSaving.saveCurrentWorkflow({ name });
if (saved) {
showCreateWorkflowSuccessToast(id);
workflowHelpers.setDocumentTitle(newName, 'IDLE');

View File

@@ -10,7 +10,6 @@ import {
} from '@/constants';
import { useClipboard } from '@/composables/useClipboard';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import type { INodeUi } from '@/Interface';
import { computed, ref, watch } from 'vue';
import { useI18n } from '@n8n/i18n';
@@ -21,9 +20,8 @@ const props = defineProps<{
nodeTypeDescription: INodeTypeDescription | null;
}>();
const router = useRouter();
const clipboard = useClipboard();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const toast = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@@ -64,7 +64,6 @@ import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
import type { EventBus } from '@n8n/utils/event-bus';
import { createEventBus } from '@n8n/utils/event-bus';
import { useRouter } from 'vue-router';
import { useElementSize } from '@vueuse/core';
import { captureMessage } from '@sentry/vue';
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
@@ -124,8 +123,7 @@ const externalHooks = useExternalHooks();
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
const { debounce } = useDebounce();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const telemetry = useTelemetry();
const credentialsStore = useCredentialsStore();

View File

@@ -39,7 +39,6 @@ import { computedWithControl } from '@vueuse/core';
import get from 'lodash/get';
import set from 'lodash/set';
import { N8nIcon, N8nIconButton, N8nInputLabel, N8nNotice, N8nText } from '@n8n/design-system';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
const LazyFixedCollectionParameter = defineAsyncComponent(
@@ -75,8 +74,7 @@ const ndvStore = useNDVStore();
const nodeHelpers = useNodeHelpers();
const asyncLoadingError = ref(false);
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const { activeNode } = storeToRefs(ndvStore);

View File

@@ -43,7 +43,6 @@ import {
useCssModule,
watch,
} from 'vue';
import { useRouter } from 'vue-router';
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
import { useTelemetry } from '@/composables/useTelemetry';
import { onClickOutside, type VueInstance } from '@vueuse/core';
@@ -124,8 +123,7 @@ const emit = defineEmits<{
modalOpenerClick: [];
}>();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const { callDebounced } = useDebounce();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@@ -8,7 +8,6 @@ import { renderComponent } from '@/__tests__/render';
import { waitFor } from '@testing-library/vue';
import { userEvent } from '@testing-library/user-event';
import { setActivePinia } from 'pinia';
import { useRouter } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { INodeUi } from '@/Interface';
@@ -40,7 +39,7 @@ const nodes = [
const mockResolveExpression = () => {
const mock = vi.fn();
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
...workflowHelpers.useWorkflowHelpers(),
resolveExpression: mock,
});

View File

@@ -45,7 +45,7 @@ const workflowsStore = useWorkflowsStore();
const ndvStore = useNDVStore();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@@ -19,7 +19,6 @@ import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
const props = defineProps<{
isArchived: boolean;
@@ -37,8 +36,7 @@ const workflowActivate = useWorkflowActivate();
const uiStore = useUIStore();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();

View File

@@ -3,7 +3,6 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useVueFlow } from '@vue-flow/core';
import { useDebounce } from '@vueuse/core';
import { computed, type ComputedRef } from 'vue';
import { useRouter } from 'vue-router';
export function useNodeSettingsInCanvas(): ComputedRef<number | undefined> {
const settingsStore = useSettingsStore();
@@ -15,8 +14,7 @@ export function useNodeSettingsInCanvas(): ComputedRef<number | undefined> {
return computed(() => undefined);
}
const router = useRouter();
const { editableWorkflow } = useCanvasOperations({ router });
const { editableWorkflow } = useCanvasOperations();
const viewFlow = useVueFlow({ id: editableWorkflow.value.id });
const zoom = computed(() => viewFlow.viewport.value.zoom);
const debouncedZoom = useDebounce(zoom, 100);

View File

@@ -39,7 +39,7 @@ const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const logsStore = useLogsStore();
const { runEntireWorkflow } = useRunWorkflow({ router });
const { startChat } = useCanvasOperations({ router });
const { startChat } = useCanvasOperations();
const isChatOpen = computed(() => logsStore.isOpen);
const isExecuting = computed(() => workflowsStore.isWorkflowRunning);

View File

@@ -7,9 +7,9 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
import type { IWorkflowSettings } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useI18n } from '@n8n/i18n';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
interface IWorkflowSaveSettings {
saveFailedExecutions: boolean;
@@ -28,7 +28,7 @@ const props = withDefaults(
const i18n = useI18n();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowSaving = useWorkflowSaving({ router });
const locale = useI18n();
const settingsStore = useSettingsStore();
@@ -177,7 +177,7 @@ async function onSaveWorkflowClick(): Promise<void> {
if (!currentId) {
return;
}
const saved = await workflowHelpers.saveCurrentWorkflow({
const saved = await workflowSaving.saveCurrentWorkflow({
id: currentId,
name: workflowName.value,
tags: currentWorkflowTagIds.value,

View File

@@ -8,7 +8,6 @@ import type {
INode,
} from 'n8n-workflow';
import { useWorkflowHelpers } from './useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { executionDataToJson, getMainAuthField, getNodeAuthOptions } from '@/utils/nodeTypesUtils';
@@ -30,7 +29,7 @@ export const useAIAssistantHelpers = () => {
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
const workflowHelpers = useWorkflowHelpers();
const locale = useI18n();
/**

View File

@@ -31,7 +31,6 @@ import {
mockNode,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { useRouter } from 'vue-router';
import { mock } from 'vitest-mock-extended';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
@@ -55,14 +54,6 @@ import type { CanvasLayoutEvent } from './useCanvasLayout';
import { useTelemetry } from './useTelemetry';
import { useToast } from '@/composables/useToast';
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<{}>();
return {
...actual,
useRouter: () => ({}),
};
});
vi.mock('n8n-workflow', async (importOriginal) => {
const actual = await importOriginal<{}>();
return {
@@ -105,8 +96,6 @@ vi.mock('@/composables/useToast', () => {
});
describe('useCanvasOperations', () => {
const router = useRouter();
const workflowId = 'test';
const initialState = {
[STORES.NODE_TYPES]: {},
@@ -144,7 +133,7 @@ describe('useCanvasOperations', () => {
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };
const { requireNodeTypeDescription } = useCanvasOperations({ router });
const { requireNodeTypeDescription } = useCanvasOperations();
const result = requireNodeTypeDescription(type, version);
expect(result).toBe(expectedDescription);
@@ -157,7 +146,7 @@ describe('useCanvasOperations', () => {
nodeTypesStore.nodeTypes = { [type]: { 2: expectedDescription } };
const { requireNodeTypeDescription } = useCanvasOperations({ router });
const { requireNodeTypeDescription } = useCanvasOperations();
const result = requireNodeTypeDescription(type);
expect(result).toBe(expectedDescription);
@@ -166,7 +155,7 @@ describe('useCanvasOperations', () => {
it("should return placeholder node type description if node type doesn't exist", () => {
const type = 'nonexistentType';
const { requireNodeTypeDescription } = useCanvasOperations({ router });
const { requireNodeTypeDescription } = useCanvasOperations();
const result = requireNodeTypeDescription(type);
expect(result).toEqual({
@@ -185,7 +174,7 @@ describe('useCanvasOperations', () => {
describe('addNode', () => {
it('should create node with default version when version is undefined', () => {
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
const result = addNode(
{
name: 'example',
@@ -199,7 +188,7 @@ describe('useCanvasOperations', () => {
});
it('should create node with default position when position is not provided', () => {
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
const result = addNode(
{
type: 'type',
@@ -212,7 +201,7 @@ describe('useCanvasOperations', () => {
});
it('should create node with provided position when position is provided', () => {
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
const result = addNode(
{
type: 'type',
@@ -241,7 +230,7 @@ describe('useCanvasOperations', () => {
credentialB,
]);
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
const result = addNode(
{
type: 'type',
@@ -256,7 +245,7 @@ describe('useCanvasOperations', () => {
const ndvStore = useNDVStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: 'type' });
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
addNode(
{
type: 'type',
@@ -274,7 +263,7 @@ describe('useCanvasOperations', () => {
const ndvStore = useNDVStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: STICKY_NODE_TYPE });
const { addNode } = useCanvasOperations({ router });
const { addNode } = useCanvasOperations();
addNode(
{
type: STICKY_NODE_TYPE,
@@ -294,7 +283,7 @@ describe('useCanvasOperations', () => {
const node = createTestNode({ position: [100, 100] });
const nodeTypeDescription = mockNodeTypeDescription();
const { resolveNodePosition } = useCanvasOperations({ router });
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition(node, nodeTypeDescription);
expect(position).toEqual([100, 100]);
@@ -316,7 +305,7 @@ describe('useCanvasOperations', () => {
uiStore.lastInteractedWithNodeHandle = 'inputs/main/0';
uiStore.lastCancelledConnectionPosition = [200, 200];
const { resolveNodePosition } = useCanvasOperations({ router });
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([200, 160]);
@@ -341,7 +330,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node);
const { resolveNodePosition } = useCanvasOperations({ router });
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([320, 100]);
@@ -372,7 +361,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce([NodeConnectionTypes.AiTool])
.mockReturnValueOnce([NodeConnectionTypes.AiTool]);
const { resolveNodePosition } = useCanvasOperations({ router });
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([460, 100]);
@@ -388,7 +377,7 @@ describe('useCanvasOperations', () => {
createTestNode({ id: 'trigger', position: [100, 100] }),
];
const { resolveNodePosition, lastClickPosition } = useCanvasOperations({ router });
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
lastClickPosition.value = [300, 300];
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
@@ -400,7 +389,7 @@ describe('useCanvasOperations', () => {
const node = createTestNode({ id: '0' });
const nodeTypeDescription = mockNodeTypeDescription();
const { resolveNodePosition } = useCanvasOperations({ router });
const { resolveNodePosition } = useCanvasOperations();
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
expect(position).toEqual([0, 0]);
@@ -417,7 +406,7 @@ describe('useCanvasOperations', () => {
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
const { updateNodesPosition } = useCanvasOperations({ router });
const { updateNodesPosition } = useCanvasOperations();
updateNodesPosition(events, { trackHistory: true, trackBulk: true });
expect(startRecordingUndoSpy).toHaveBeenCalled();
@@ -445,7 +434,7 @@ describe('useCanvasOperations', () => {
}),
);
const { updateNodesPosition } = useCanvasOperations({ router });
const { updateNodesPosition } = useCanvasOperations();
updateNodesPosition(events);
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
@@ -459,7 +448,7 @@ describe('useCanvasOperations', () => {
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
const { updateNodesPosition } = useCanvasOperations({ router });
const { updateNodesPosition } = useCanvasOperations();
updateNodesPosition(events, { trackHistory: false, trackBulk: false });
expect(startRecordingUndoSpy).not.toHaveBeenCalled();
@@ -484,7 +473,7 @@ describe('useCanvasOperations', () => {
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
const { tidyUp } = useCanvasOperations({ router });
const { tidyUp } = useCanvasOperations();
tidyUp(event);
expect(startRecordingUndoSpy).toHaveBeenCalled();
@@ -519,7 +508,7 @@ describe('useCanvasOperations', () => {
}),
);
const { tidyUp } = useCanvasOperations({ router });
const { tidyUp } = useCanvasOperations();
tidyUp(event);
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
@@ -540,7 +529,7 @@ describe('useCanvasOperations', () => {
},
};
const { tidyUp } = useCanvasOperations({ router });
const { tidyUp } = useCanvasOperations();
tidyUp(event);
expect(useTelemetry().track).toHaveBeenCalledWith(
@@ -569,7 +558,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValueOnce(node);
const { updateNodePosition } = useCanvasOperations({ router });
const { updateNodePosition } = useCanvasOperations();
updateNodePosition(id, position);
expect(workflowsStore.setNodePositionById).toHaveBeenCalledWith(id, [position.x, position.y]);
@@ -585,7 +574,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
uiStore.lastSelectedNode = '';
const { setNodeSelected } = useCanvasOperations({ router });
const { setNodeSelected } = useCanvasOperations();
setNodeSelected(nodeId);
expect(uiStore.lastSelectedNode).toBe(nodeName);
@@ -598,7 +587,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
uiStore.lastSelectedNode = 'Existing Node';
const { setNodeSelected } = useCanvasOperations({ router });
const { setNodeSelected } = useCanvasOperations();
setNodeSelected(nodeId);
expect(uiStore.lastSelectedNode).toBe('Existing Node');
@@ -608,7 +597,7 @@ describe('useCanvasOperations', () => {
const uiStore = useUIStore();
uiStore.lastSelectedNode = 'Existing Node';
const { setNodeSelected } = useCanvasOperations({ router });
const { setNodeSelected } = useCanvasOperations();
setNodeSelected();
expect(uiStore.lastSelectedNode).toBe('');
@@ -633,7 +622,7 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
await addNodes(nodes, {});
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
@@ -670,7 +659,7 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
await addNodes(nodes, { position: [50, 60] });
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
@@ -711,7 +700,7 @@ describe('useCanvasOperations', () => {
}),
);
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
await addNodes(nodes, {});
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
@@ -736,7 +725,7 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
const added = await addNodes(nodes, {});
expect(added.length).toBe(2);
});
@@ -756,7 +745,7 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
await addNodes(nodes, { keepPristine: false });
expect(uiStore.stateIsDirty).toEqual(true);
@@ -777,7 +766,7 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
const { addNodes } = useCanvasOperations();
await addNodes(nodes, { keepPristine: true });
expect(uiStore.stateIsDirty).toEqual(false);
@@ -792,7 +781,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValueOnce(node);
const removeNodeByIdSpy = vi.spyOn(workflowsStore, 'removeNodeById');
const { revertAddNode } = useCanvasOperations({ router });
const { revertAddNode } = useCanvasOperations();
await revertAddNode(node.name);
expect(removeNodeByIdSpy).toHaveBeenCalledWith(node.id);
@@ -818,7 +807,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(node);
const { deleteNode } = useCanvasOperations({ router });
const { deleteNode } = useCanvasOperations();
deleteNode(id, { trackHistory: true });
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
@@ -847,7 +836,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(node);
const { deleteNode } = useCanvasOperations({ router });
const { deleteNode } = useCanvasOperations();
deleteNode(id, { trackHistory: false });
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
@@ -916,7 +905,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
const { deleteNode } = useCanvasOperations({ router });
const { deleteNode } = useCanvasOperations();
deleteNode(nodes[1].id);
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
@@ -992,7 +981,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
const { deleteNode } = useCanvasOperations({ router });
const { deleteNode } = useCanvasOperations();
deleteNode(nodes[1].id);
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
@@ -1013,7 +1002,7 @@ describe('useCanvasOperations', () => {
parameters: {},
});
const { revertDeleteNode } = useCanvasOperations({ router });
const { revertDeleteNode } = useCanvasOperations();
revertDeleteNode(node);
expect(workflowsStore.addNode).toHaveBeenCalledWith(node);
@@ -1033,7 +1022,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
const { renameNode } = useCanvasOperations({ router });
const { renameNode } = useCanvasOperations();
await renameNode(oldName, newName);
expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
@@ -1047,7 +1036,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
const { renameNode } = useCanvasOperations({ router });
const { renameNode } = useCanvasOperations();
await renameNode(oldName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
@@ -1067,7 +1056,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName });
ndvStore.activeNodeName = currentName;
const { revertRenameNode } = useCanvasOperations({ router });
const { revertRenameNode } = useCanvasOperations();
await revertRenameNode(currentName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
@@ -1080,7 +1069,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
const { revertRenameNode } = useCanvasOperations({ router });
const { revertRenameNode } = useCanvasOperations();
await revertRenameNode(oldName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
@@ -1096,7 +1085,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
ndvStore.activeNodeName = '';
const { setNodeActive } = useCanvasOperations({ router });
const { setNodeActive } = useCanvasOperations();
setNodeActive(nodeId);
expect(ndvStore.activeNodeName).toBe(nodeName);
@@ -1109,7 +1098,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
ndvStore.activeNodeName = 'Existing Node';
const { setNodeActive } = useCanvasOperations({ router });
const { setNodeActive } = useCanvasOperations();
setNodeActive(nodeId);
expect(ndvStore.activeNodeName).toBe('Existing Node');
@@ -1121,7 +1110,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockImplementation(() => node);
const { setNodeActive } = useCanvasOperations({ router });
const { setNodeActive } = useCanvasOperations();
setNodeActive(node.id);
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(node.name, false);
@@ -1134,7 +1123,7 @@ describe('useCanvasOperations', () => {
const nodeName = 'Node 1';
ndvStore.activeNodeName = '';
const { setNodeActiveByName } = useCanvasOperations({ router });
const { setNodeActiveByName } = useCanvasOperations();
setNodeActiveByName(nodeName);
expect(ndvStore.activeNodeName).toBe(nodeName);
@@ -1150,7 +1139,7 @@ describe('useCanvasOperations', () => {
];
workflowsStore.getNodesByIds.mockReturnValue(nodes);
const { toggleNodesDisabled } = useCanvasOperations({ router });
const { toggleNodesDisabled } = useCanvasOperations();
toggleNodesDisabled([nodes[0].id, nodes[1].id], {
trackHistory: true,
trackBulk: true,
@@ -1173,7 +1162,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName.mockReturnValue(node);
const updateNodePropertiesSpy = vi.spyOn(workflowsStore, 'updateNodeProperties');
const { revertToggleNodeDisabled } = useCanvasOperations({ router });
const { revertToggleNodeDisabled } = useCanvasOperations();
revertToggleNodeDisabled(nodeName);
expect(updateNodePropertiesSpy).toHaveBeenCalledWith({
@@ -1249,7 +1238,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType);
const { addConnections } = useCanvasOperations({ router });
const { addConnections } = useCanvasOperations();
await addConnections(connections);
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
@@ -1272,7 +1261,7 @@ describe('useCanvasOperations', () => {
const uiStore = mockedStore(useUIStore);
const connections: CanvasConnection[] = [];
const { addConnections } = useCanvasOperations({ router });
const { addConnections } = useCanvasOperations();
await addConnections(connections, { keepPristine: false });
expect(uiStore.stateIsDirty).toBe(true);
@@ -1282,7 +1271,7 @@ describe('useCanvasOperations', () => {
const uiStore = mockedStore(useUIStore);
const connections: CanvasConnection[] = [];
const { addConnections } = useCanvasOperations({ router });
const { addConnections } = useCanvasOperations();
await addConnections(connections, { keepPristine: true });
expect(uiStore.stateIsDirty).toBe(false);
@@ -1297,7 +1286,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValueOnce(undefined);
const { createConnection } = useCanvasOperations({ router });
const { createConnection } = useCanvasOperations();
createConnection(connection);
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
@@ -1313,7 +1302,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(createTestNode())
.mockReturnValueOnce(undefined);
const { createConnection } = useCanvasOperations({ router });
const { createConnection } = useCanvasOperations();
createConnection(connection);
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
@@ -1361,7 +1350,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
const { createConnection, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
@@ -1418,7 +1407,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
const { createConnection, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
@@ -1441,7 +1430,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeByName.mockReturnValue(testNode);
workflowsStore.getNodeById.mockReturnValue(testNode);
const { revertCreateConnection } = useCanvasOperations({ router });
const { revertCreateConnection } = useCanvasOperations();
revertCreateConnection(connection);
expect(workflowsStore.removeConnection).toHaveBeenCalled();
@@ -1492,7 +1481,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName],
);
const { isConnectionAllowed } = useCanvasOperations({ router });
const { isConnectionAllowed } = useCanvasOperations();
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
});
@@ -1539,7 +1528,7 @@ describe('useCanvasOperations', () => {
})[nodeTypeName],
);
const { isConnectionAllowed } = useCanvasOperations({ router });
const { isConnectionAllowed } = useCanvasOperations();
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
});
@@ -1579,7 +1568,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1630,7 +1619,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1690,7 +1679,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1750,7 +1739,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1810,7 +1799,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1871,7 +1860,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1929,7 +1918,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
@@ -1972,7 +1961,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
nodeTypesStore.getNodeType = vi.fn(
@@ -1995,7 +1984,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(createTestNode());
const { deleteConnection } = useCanvasOperations({ router });
const { deleteConnection } = useCanvasOperations();
deleteConnection(connection);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2009,7 +1998,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(createTestNode())
.mockReturnValueOnce(undefined);
const { deleteConnection } = useCanvasOperations({ router });
const { deleteConnection } = useCanvasOperations();
deleteConnection(connection);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2039,7 +2028,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
const { deleteConnection } = useCanvasOperations({ router });
const { deleteConnection } = useCanvasOperations();
deleteConnection(connection);
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
@@ -2060,7 +2049,7 @@ describe('useCanvasOperations', () => {
{ node: 'targetNode', type: NodeConnectionTypes.Main, index: 2 },
];
const { revertDeleteConnection } = useCanvasOperations({ router });
const { revertDeleteConnection } = useCanvasOperations();
revertDeleteConnection(connection);
expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection });
@@ -2073,7 +2062,7 @@ describe('useCanvasOperations', () => {
const nonexistentId = 'nonexistent';
workflowsStore.getNodeById.mockReturnValue(undefined);
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(nonexistentId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2088,7 +2077,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(node);
nodeTypesStore.getNodeType = () => null;
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(nodeId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2145,7 +2134,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(targetNodeId);
await nextTick();
@@ -2209,7 +2198,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(targetNodeId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2222,7 +2211,7 @@ describe('useCanvasOperations', () => {
const nonexistentId = 'nonexistent';
workflowsStore.getNodeById.mockReturnValue(undefined);
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(nonexistentId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2237,7 +2226,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodeById.mockReturnValue(node);
nodeTypesStore.getNodeType = () => null;
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(nodeId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2294,7 +2283,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(sourceNodeId);
await nextTick();
@@ -2358,7 +2347,7 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(sourceNodeId);
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
@@ -2368,7 +2357,7 @@ describe('useCanvasOperations', () => {
describe('deleteConnectionsByNodeId', () => {
it('should delete all connections for a given node ID', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
const { deleteConnectionsByNodeId } = useCanvasOperations();
const node1 = createTestNode({ id: 'node1', name: 'Node 1' });
const node2 = createTestNode({ id: 'node2', name: 'Node 1' });
@@ -2410,7 +2399,7 @@ describe('useCanvasOperations', () => {
it('should not delete connections if node ID does not exist', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
const { deleteConnectionsByNodeId } = useCanvasOperations();
const nodeId = 'nonexistent';
workflowsStore.getNodeById.mockReturnValue(undefined);
@@ -2422,7 +2411,7 @@ describe('useCanvasOperations', () => {
it('should delete all connections of a node with multiple connections', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
const { deleteConnectionsByNodeId } = useCanvasOperations();
const sourceNode = createTestNode({ id: 'source', name: 'Source Node' });
const targetNode = createTestNode({ id: 'target', name: 'Target Node' });
@@ -2516,7 +2505,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
workflowsStore.getWorkflow.mockReturnValue(workflowObject);
const canvasOperations = useCanvasOperations({ router });
const canvasOperations = useCanvasOperations();
const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);
expect(duplicatedNodeIds.length).toBe(2);
@@ -2540,7 +2529,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodesByIds.mockReturnValue(nodes);
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
const { copyNodes } = useCanvasOperations({ router });
const { copyNodes } = useCanvasOperations();
await copyNodes(['1', '2']);
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
@@ -2563,7 +2552,7 @@ describe('useCanvasOperations', () => {
workflowsStore.getNodesByIds.mockReturnValue(nodes);
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
const { cutNodes } = useCanvasOperations({ router });
const { cutNodes } = useCanvasOperations();
await cutNodes(['1', '2']);
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
@@ -2578,7 +2567,7 @@ describe('useCanvasOperations', () => {
it("should set webhookId if it doesn't already exist", () => {
const node = mock<INodeUi>({ webhookId: undefined });
const { resolveNodeWebhook } = useCanvasOperations({ router });
const { resolveNodeWebhook } = useCanvasOperations();
resolveNodeWebhook(node, nodeTypeDescription);
expect(node.webhookId).toBeDefined();
@@ -2587,7 +2576,7 @@ describe('useCanvasOperations', () => {
it('should not set webhookId if it already exists', () => {
const node = mock<INodeUi>({ webhookId: 'random-id' });
const { resolveNodeWebhook } = useCanvasOperations({ router });
const { resolveNodeWebhook } = useCanvasOperations();
resolveNodeWebhook(node, nodeTypeDescription);
expect(node.webhookId).toBe('random-id');
@@ -2596,7 +2585,7 @@ describe('useCanvasOperations', () => {
it("should not set webhookId if node description doesn't define any webhooks", () => {
const node = mock<INodeUi>({ webhookId: undefined });
const { resolveNodeWebhook } = useCanvasOperations({ router });
const { resolveNodeWebhook } = useCanvasOperations();
resolveNodeWebhook(node, mock<INodeTypeDescription>({ webhooks: [] }));
expect(node.webhookId).toBeUndefined();
@@ -2611,7 +2600,7 @@ describe('useCanvasOperations', () => {
parameters: { path: '' },
});
const { resolveNodeWebhook } = useCanvasOperations({ router });
const { resolveNodeWebhook } = useCanvasOperations();
resolveNodeWebhook(node, nodeTypeDescription);
expect(node.webhookId).toBe('random-id');
@@ -2628,7 +2617,7 @@ describe('useCanvasOperations', () => {
connections: {},
});
const { initializeWorkspace } = useCanvasOperations({ router });
const { initializeWorkspace } = useCanvasOperations();
initializeWorkspace(workflow);
expect(workflowsStore.setNodes).toHaveBeenCalled();
@@ -2659,7 +2648,7 @@ describe('useCanvasOperations', () => {
connections: {},
});
const { initializeWorkspace } = useCanvasOperations({ router });
const { initializeWorkspace } = useCanvasOperations();
initializeWorkspace(workflow);
expect(workflow.nodes[0].parameters).toEqual({ value: true });
@@ -2679,7 +2668,7 @@ describe('useCanvasOperations', () => {
};
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const { filterConnectionsByNodes } = useCanvasOperations();
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual(connections);
@@ -2697,7 +2686,7 @@ describe('useCanvasOperations', () => {
};
const includeNodeNames = new Set<string>();
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const { filterConnectionsByNodes } = useCanvasOperations();
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({
@@ -2717,7 +2706,7 @@ describe('useCanvasOperations', () => {
};
const includeNodeNames = new Set<string>(['node1']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const { filterConnectionsByNodes } = useCanvasOperations();
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({
@@ -2732,7 +2721,7 @@ describe('useCanvasOperations', () => {
const connections: INodeConnections = {};
const includeNodeNames = new Set<string>(['node1']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const { filterConnectionsByNodes } = useCanvasOperations();
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({});
@@ -2750,7 +2739,7 @@ describe('useCanvasOperations', () => {
};
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const { filterConnectionsByNodes } = useCanvasOperations();
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({
@@ -2763,7 +2752,7 @@ describe('useCanvasOperations', () => {
it('should initialize workspace and set execution data when execution is found', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const { openExecution } = useCanvasOperations({ router });
const { openExecution } = useCanvasOperations();
const executionId = '123';
const executionData: IExecutionResponse = {
@@ -2788,7 +2777,7 @@ describe('useCanvasOperations', () => {
it('should throw error when execution data is undefined', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const executionId = '123';
const { openExecution } = useCanvasOperations({ router });
const { openExecution } = useCanvasOperations();
workflowsStore.getExecution.mockResolvedValue(undefined);
@@ -2799,7 +2788,7 @@ describe('useCanvasOperations', () => {
it('should clear workflow pin data if execution mode is not manual', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const { openExecution } = useCanvasOperations({ router });
const { openExecution } = useCanvasOperations();
const executionId = '123';
const executionData: IExecutionResponse = {
@@ -2820,7 +2809,7 @@ describe('useCanvasOperations', () => {
});
it('should show an error notification for failed executions', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const { openExecution } = useCanvasOperations({ router });
const { openExecution } = useCanvasOperations();
const toast = useToast();
const executionId = '123';
@@ -2909,7 +2898,7 @@ describe('useCanvasOperations', () => {
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
});
const { connectAdjacentNodes } = useCanvasOperations({ router });
const { connectAdjacentNodes } = useCanvasOperations();
connectAdjacentNodes(nodeB.id, { trackHistory: true });
// Check that A was connected directly to C
@@ -2979,7 +2968,7 @@ describe('useCanvasOperations', () => {
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
});
const { connectAdjacentNodes } = useCanvasOperations({ router });
const { connectAdjacentNodes } = useCanvasOperations();
connectAdjacentNodes(nodeB.id, { trackHistory: true });
// Check that A was connected directly to C
@@ -3016,7 +3005,7 @@ describe('useCanvasOperations', () => {
});
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
const { connectAdjacentNodes } = useCanvasOperations({ router });
const { connectAdjacentNodes } = useCanvasOperations();
connectAdjacentNodes(nodeB.id);
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
@@ -3044,7 +3033,7 @@ describe('useCanvasOperations', () => {
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
});
const { connectAdjacentNodes } = useCanvasOperations({ router });
const { connectAdjacentNodes } = useCanvasOperations();
connectAdjacentNodes(nodeB.id);
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
@@ -3086,7 +3075,7 @@ describe('useCanvasOperations', () => {
},
};
const { importTemplate } = useCanvasOperations({ router });
const { importTemplate } = useCanvasOperations();
const templateId = 'template-id';
const templateName = 'template name';

View File

@@ -102,7 +102,6 @@ import type {
} from 'n8n-workflow';
import { deepCopy, NodeConnectionTypes, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
import { computed, nextTick, ref } from 'vue';
import type { useRouter } from 'vue-router';
import { useClipboard } from '@/composables/useClipboard';
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
import { isPresent } from '../utils/typesUtils';
@@ -140,7 +139,7 @@ type AddNodeOptions = AddNodesBaseOptions & {
isAutoAdd?: boolean;
};
export function useCanvasOperations({ router }: { router: ReturnType<typeof useRouter> }) {
export function useCanvasOperations() {
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const credentialsStore = useCredentialsStore();
@@ -158,7 +157,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const i18n = useI18n();
const toast = useToast();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const nodeHelpers = useNodeHelpers();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();

View File

@@ -2,7 +2,7 @@ import { START_NODE_TYPE } from '@/constants';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { useCanvasOperations } from './useCanvasOperations';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -13,8 +13,7 @@ export function useClearExecutionButtonVisible() {
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
const router = useRouter();
const { editableWorkflow } = useCanvasOperations({ router });
const { editableWorkflow } = useCanvasOperations();
const nodeTypesStore = useNodeTypesStore();
const isReadOnlyEnvironment = computed(() => sourceControlStore.preferences.branchReadOnly);
const allTriggerNodesDisabled = computed(() =>

View File

@@ -9,7 +9,6 @@ import { fireEvent, waitFor } from '@testing-library/vue';
import { setActivePinia } from 'pinia';
import { beforeEach, describe, vi } from 'vitest';
import { defineComponent, h, ref, toValue } from 'vue';
import { useRouter } from 'vue-router';
import { useExpressionEditor } from './useExpressionEditor';
vi.mock('@/composables/useAutocompleteTelemetry', () => ({
@@ -26,7 +25,7 @@ describe('useExpressionEditor', () => {
const mockResolveExpression = () => {
const mock = vi.fn();
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
...workflowHelpers.useWorkflowHelpers(),
resolveExpression: mock,
});

View File

@@ -35,7 +35,6 @@ import {
import { EditorView, type ViewUpdate } from '@codemirror/view';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '../stores/workflows.store';
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
@@ -62,8 +61,7 @@ export const useExpressionEditor = ({
}) => {
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const editor = ref<EditorView>();
const hasFocus = ref(false);

View File

@@ -15,12 +15,7 @@ import { type FrontendSettings } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
import { NodeConnectionTypes, type IConnections, type IRunData } from 'n8n-workflow';
import { defineComponent } from 'vue';
import {
createRouter,
createWebHistory,
useRouter,
type RouteLocationNormalizedLoaded,
} from 'vue-router';
import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } from 'vue-router';
describe(useNodeDirtiness, () => {
let nodeTypeStore: ReturnType<typeof useNodeTypesStore>;
@@ -42,7 +37,7 @@ describe(useNodeDirtiness, () => {
workflowsStore = useWorkflowsStore();
settingsStore = useSettingsStore();
historyHelper = useHistoryHelper({} as RouteLocationNormalizedLoaded);
canvasOperations = useCanvasOperations({ router: useRouter() });
canvasOperations = useCanvasOperations();
uiStore = useUIStore();
nodeTypeStore.setNodeTypes(defaultNodeDescriptions);

View File

@@ -26,6 +26,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
export type SimplifiedExecution = Pick<
IExecutionResponse,
@@ -84,7 +85,7 @@ export async function executionFinished(
};
} else {
if (data.status === 'success') {
handleExecutionFinishedSuccessfully(data.workflowId, options);
handleExecutionFinishedSuccessfully(data.workflowId);
successToastAlreadyShown = true;
}
@@ -101,9 +102,9 @@ export async function executionFinished(
if (execution.data?.waitTill !== undefined) {
handleExecutionFinishedWithWaitTill(options);
} else if (execution.status === 'error' || execution.status === 'canceled') {
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, options);
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
} else {
handleExecutionFinishedWithOther(successToastAlreadyShown, options);
handleExecutionFinishedWithOther(successToastAlreadyShown);
}
setRunExecutionData(execution, runExecutionData);
@@ -234,7 +235,8 @@ export function handleExecutionFinishedWithWaitTill(options: {
}) {
const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const workflowHelpers = useWorkflowHelpers(options);
const workflowSaving = useWorkflowSaving(options);
const workflowHelpers = useWorkflowHelpers();
const workflowObject = workflowsStore.getCurrentWorkflow();
const workflowSettings = workflowsStore.workflowSettings;
@@ -247,7 +249,7 @@ export function handleExecutionFinishedWithWaitTill(options: {
globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
key: 'open-settings',
action: async () => {
if (workflowsStore.isNewWorkflow) await workflowHelpers.saveAsNewWorkflow();
if (workflowsStore.isNewWorkflow) await workflowSaving.saveAsNewWorkflow();
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
},
});
@@ -263,13 +265,12 @@ export function handleExecutionFinishedWithWaitTill(options: {
export function handleExecutionFinishedWithErrorOrCanceled(
execution: SimplifiedExecution,
runExecutionData: IRunExecutionData,
options: { router: ReturnType<typeof useRouter> },
) {
const toast = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore();
const workflowHelpers = useWorkflowHelpers(options);
const workflowHelpers = useWorkflowHelpers();
const workflowObject = workflowsStore.getCurrentWorkflow();
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR');
@@ -339,12 +340,9 @@ export function handleExecutionFinishedWithErrorOrCanceled(
* immediately, even though we still need to fetch and deserialize the
* full execution data, to minimize perceived latency.
*/
export function handleExecutionFinishedSuccessfully(
workflowId: string,
options: { router: ReturnType<typeof useRouter> },
) {
export function handleExecutionFinishedSuccessfully(workflowId: string) {
const workflowsStore = useWorkflowsStore();
const workflowHelpers = useWorkflowHelpers(options);
const workflowHelpers = useWorkflowHelpers();
const toast = useToast();
const i18n = useI18n();
@@ -359,14 +357,11 @@ export function handleExecutionFinishedSuccessfully(
/**
* Handle the case when the workflow execution finished successfully.
*/
export function handleExecutionFinishedWithOther(
successToastAlreadyShown: boolean,
options: { router: ReturnType<typeof useRouter> },
) {
export function handleExecutionFinishedWithOther(successToastAlreadyShown: boolean) {
const workflowsStore = useWorkflowsStore();
const toast = useToast();
const i18n = useI18n();
const workflowHelpers = useWorkflowHelpers(options);
const workflowHelpers = useWorkflowHelpers();
const nodeTypesStore = useNodeTypesStore();
const workflowObject = workflowsStore.getCurrentWorkflow();

View File

@@ -37,9 +37,9 @@ export async function executionRecovered(
if (execution.data?.waitTill !== undefined) {
handleExecutionFinishedWithWaitTill(options);
} else if (execution.status === 'error' || execution.status === 'canceled') {
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, options);
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
} else {
handleExecutionFinishedWithOther(false, options);
handleExecutionFinishedWithOther(false);
}
setRunExecutionData(execution, runExecutionData);

View File

@@ -2,7 +2,6 @@ import { defineComponent, h, nextTick, ref, toValue } from 'vue';
import { useResolvedExpression } from './useResolvedExpression';
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
import { renderComponent } from '../__tests__/render';
import { useRouter } from 'vue-router';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@@ -25,7 +24,7 @@ async function renderTestComponent(...options: Parameters<typeof useResolvedExpr
const mockResolveExpression = () => {
const mock = vi.fn();
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
...workflowHelpers.useWorkflowHelpers(),
resolveExpression: mock,
});

View File

@@ -5,7 +5,6 @@ import { isExpression as isExpressionUtil, stringifyExpressionResult } from '@/u
import debounce from 'lodash/debounce';
import { createResultError, createResultOk, type IDataObject, type Result } from 'n8n-workflow';
import { computed, onMounted, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
import { useRouter } from 'vue-router';
import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers';
export function useResolvedExpression({
@@ -22,8 +21,7 @@ export function useResolvedExpression({
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const router = useRouter();
const { resolveExpression } = useWorkflowHelpers({ router });
const { resolveExpression } = useWorkflowHelpers();
const resolvedExpression = ref<unknown>(null);
const resolvedExpressionString = ref('');

View File

@@ -152,7 +152,7 @@ describe('useRunWorkflow({ router })', () => {
agentRequestStore = useAgentRequestStore();
router = useRouter();
workflowHelpers = useWorkflowHelpers({ router });
workflowHelpers = useWorkflowHelpers();
});
afterEach(() => {
@@ -236,7 +236,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers({ router })).getWorkflowDataToSave.mockResolvedValue({
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: 'Slack',
@@ -267,7 +267,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers({ router })).getWorkflowDataToSave.mockResolvedValue({
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: 'Slack',

View File

@@ -44,10 +44,12 @@ import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
import { useCanvasOperations } from './useCanvasOperations';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useWorkflowSaving } from './useWorkflowSaving';
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
const nodeHelpers = useNodeHelpers();
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
const workflowHelpers = useWorkflowHelpers();
const workflowSaving = useWorkflowSaving({ router: useRunWorkflowOpts.router });
const i18n = useI18n();
const toast = useToast();
const telemetry = useTelemetry();
@@ -60,7 +62,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const workflowsStore = useWorkflowsStore();
const executionsStore = useExecutionsStore();
const { dirtinessByName } = useNodeDirtiness();
const { startChat } = useCanvasOperations({ router: useRunWorkflowOpts.router });
const { startChat } = useCanvasOperations();
function sortNodesByYPosition(nodes: string[]) {
return [...nodes].sort((a, b) => {
@@ -144,7 +146,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const runData = workflowsStore.getWorkflowRunData;
if (workflowsStore.isNewWorkflow) {
await workflowHelpers.saveCurrentWorkflow();
await workflowSaving.saveCurrentWorkflow();
}
const workflowData = await workflowHelpers.getWorkflowDataToSave();

View File

@@ -15,12 +15,14 @@ import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n';
import { ref } from 'vue';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useWorkflowSaving } from './useWorkflowSaving';
export function useWorkflowActivate() {
const updatingWorkflowActivation = ref(false);
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const workflowSaving = useWorkflowSaving({ router });
const workflowsStore = useWorkflowsStore();
const uiStore = useUIStore();
const telemetry = useTelemetry();
@@ -41,7 +43,7 @@ export function useWorkflowActivate() {
let currWorkflowId: string | undefined = workflowId;
if (!currWorkflowId || currWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
const saved = await workflowHelpers.saveCurrentWorkflow();
const saved = await workflowSaving.saveCurrentWorkflow();
if (!saved) {
updatingWorkflowActivation.value = false;
return false; // Return false if save failed

View File

@@ -37,7 +37,7 @@ export function useWorkflowExtraction() {
const toast = useToast();
const router = useRouter();
const historyStore = useHistoryStore();
const canvasOperations = useCanvasOperations({ router });
const canvasOperations = useCanvasOperations();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@@ -1,77 +1,21 @@
import type {
IExecutionResponse,
IWorkflowData,
IWorkflowDataUpdate,
IWorkflowDb,
} from '@/Interface';
import type { IExecutionResponse, IWorkflowData, IWorkflowDb } from '@/Interface';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import router from '@/router';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useTagsStore } from '@/stores/tags.store';
import { useUIStore } from '@/stores/ui.store';
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
import { createTestWorkflow } from '@/__tests__/mocks';
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks';
import { mockedStore } from '@/__tests__/utils';
import { nodeTypes } from '@/components/CanvasChat/__test__/data';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { CHAT_TRIGGER_NODE_TYPE } from '@/constants';
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
name: 'Duplicate webhook test',
active: false,
nodes: [
{
parameters: {
path: '5340ae49-2c96-4492-9073-7744d2e52b8a',
options: {},
},
id: 'c1e1b6e7-df13-41b1-95f6-42903b85e438',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [680, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
{
parameters: {
path: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
options: {},
},
id: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
name: 'Webhook 2',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [700, 40],
webhookId: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
},
{
parameters: {
resume: 'webhook',
options: {
webhookSuffix: '/test',
},
},
id: '979d8443-51b1-48e2-b239-acf399b66509',
name: 'Wait',
type: 'n8n-nodes-base.wait',
typeVersion: 1.1,
position: [900, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
],
connections: {},
});
describe('useWorkflowHelpers', () => {
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
let tagsStore: ReturnType<typeof useTagsStore>;
let uiStore: ReturnType<typeof useUIStore>;
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
beforeAll(() => {
setActivePinia(createTestingPinia());
@@ -99,7 +43,7 @@ describe('useWorkflowHelpers', () => {
options: {},
infoMessage: '',
};
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const resolvedParameters =
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
expect(resolvedParameters.url).toHaveProperty('resolvedExpressionValue');
@@ -123,7 +67,7 @@ describe('useWorkflowHelpers', () => {
includeOtherFields: false,
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const resolvedParameters =
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
expect(resolvedParameters).toHaveProperty('assignments');
@@ -164,7 +108,7 @@ describe('useWorkflowHelpers', () => {
looseTypeValidation: false,
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
@@ -193,7 +137,7 @@ describe('useWorkflowHelpers', () => {
combineFilters: 'AND',
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
@@ -232,7 +176,7 @@ describe('useWorkflowHelpers', () => {
combineFilters: 'AND',
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
@@ -242,55 +186,9 @@ describe('useWorkflowHelpers', () => {
});
});
describe('saveAsNewWorkflow', () => {
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: false,
data: workflow,
});
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Expect webhookIds and paths to be the same as in the original workflow
expect(webHookIdsPreSave).toEqual(webHookIdsPostSave);
expect(pathsPreSave).toEqual(pathsPostSave);
});
it('should respect `resetWebhookUrls: true` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: true,
data: workflow,
});
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Now, expect webhookIds and paths to be different
expect(webHookIdsPreSave).not.toEqual(webHookIdsPostSave);
expect(pathsPreSave).not.toEqual(pathsPostSave);
});
});
describe('initState', () => {
it('should initialize workflow state with provided data', () => {
const { initState } = useWorkflowHelpers({ router });
const { initState } = useWorkflowHelpers();
const workflowData = createTestWorkflow({
id: '1',
@@ -344,7 +242,7 @@ describe('useWorkflowHelpers', () => {
});
it('should handle missing `usedCredentials` and `sharedWithProjects` gracefully', () => {
const { initState } = useWorkflowHelpers({ router });
const { initState } = useWorkflowHelpers();
const workflowData = createTestWorkflow({
id: '1',
@@ -365,7 +263,7 @@ describe('useWorkflowHelpers', () => {
});
it('should handle missing `tags` gracefully', () => {
const { initState } = useWorkflowHelpers({ router });
const { initState } = useWorkflowHelpers();
const workflowData = createTestWorkflow({
id: '1',
@@ -387,7 +285,7 @@ describe('useWorkflowHelpers', () => {
describe('checkConflictingWebhooks', () => {
it('should return null if no conflicts', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [],
@@ -396,7 +294,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return conflicting webhook data and workflow id is different', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [
@@ -433,7 +331,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return null if webhook already exist but workflow id is the same', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [
@@ -456,7 +354,7 @@ describe('useWorkflowHelpers', () => {
});
it('should call getWorkflowDataToSave if state is dirty', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
uiStore.stateIsDirty = true;
vi.spyOn(workflowHelpers, 'getWorkflowDataToSave').mockResolvedValue({
nodes: [],
@@ -467,7 +365,7 @@ describe('useWorkflowHelpers', () => {
describe('executeData', () => {
it('should return empty execute data if no parent nodes', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes: string[] = [];
const currentNode = 'Set';
@@ -484,7 +382,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return the correct execution data with one parent node', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['Start'];
const currentNode = 'Set';
@@ -555,7 +453,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return the correct execution data with multiple parent nodes, only one with execution data', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['Parent A', 'Parent B'];
const currentNode = 'Set';
@@ -626,7 +524,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return the correct execution data with multiple parent nodes, all with execution data', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['Parent A', 'Parent B'];
const currentNode = 'Set';
@@ -722,7 +620,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return data from pinnedWorkflowData if available', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['ParentNode'];
const currentNode = 'CurrentNode';
@@ -741,7 +639,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return data from getWorkflowRunData if pinnedWorkflowData is not available', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['ParentNode'];
const currentNode = 'CurrentNode';
@@ -778,7 +676,7 @@ describe('useWorkflowHelpers', () => {
});
});
it('should use provided parentRunIndex ', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['ParentNode'];
const currentNode = 'CurrentNode';
@@ -818,7 +716,7 @@ describe('useWorkflowHelpers', () => {
});
it('should return empty data if neither pinnedWorkflowData nor getWorkflowRunData is available', () => {
const { executeData } = useWorkflowHelpers({ router });
const { executeData } = useWorkflowHelpers();
const parentNodes = ['ParentNode'];
const currentNode = 'CurrentNode';
@@ -835,58 +733,4 @@ describe('useWorkflowHelpers', () => {
expect(result.source).toBeNull();
});
});
describe('saveCurrentWorkflow', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }));
workflowsStore = mockedStore(useWorkflowsStore);
nodeTypesStore = mockedStore(useNodeTypesStore);
nodeTypesStore.setNodeTypes(nodeTypes);
});
it('should save the current workflow', async () => {
const workflow = createTestWorkflow({
id: 'w0',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
active: true,
});
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.setWorkflow(workflow);
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
await saveCurrentWorkflow({ id: 'w0' });
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
'w0',
expect.objectContaining({ id: 'w0', active: true }),
false,
);
});
it('should include active=false in the request if the workflow has no activatable trigger node', async () => {
const workflow = createTestWorkflow({
id: 'w1',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true })],
active: true,
});
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.setWorkflow(workflow);
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
await saveCurrentWorkflow({ id: 'w1' });
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
'w1',
expect.objectContaining({ id: 'w1', active: false }),
false,
);
expect(workflowsStore.setWorkflowInactive).toHaveBeenCalled();
});
});
});

View File

@@ -1,10 +1,7 @@
import {
HTTP_REQUEST_NODE_TYPE,
MODAL_CONFIRM,
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
VIEWS,
} from '@/constants';
import type {
@@ -21,7 +18,6 @@ import type {
IRunExecutionData,
IWebhookDescription,
IWorkflowDataProxyAdditionalKeys,
IWorkflowSettings,
NodeParameterValue,
Workflow,
} from 'n8n-workflow';
@@ -32,19 +28,14 @@ import type {
INodeTypesMaxCount,
INodeUi,
ITag,
IUpdateInformation,
IWorkflowData,
IWorkflowDataCreate,
IWorkflowDataUpdate,
IWorkflowDb,
NotificationOptions,
TargetItem,
WorkflowTitleStatus,
XYPosition,
} from '@/Interface';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import get from 'lodash/get';
@@ -53,19 +44,12 @@ import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { getSourceItems } from '@/utils/pairedItemUtils';
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useCanvasStore } from '@/stores/canvas.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useI18n } from '@n8n/i18n';
import type { useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { useProjectsStore } from '@/stores/projects.store';
import { useTagsStore } from '@/stores/tags.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
@@ -450,11 +434,9 @@ export function executeData(
return executeData;
}
export function useWorkflowHelpers(options: { router: ReturnType<typeof useRouter> }) {
const router = options.router;
export function useWorkflowHelpers() {
const nodeTypesStore = useNodeTypesStore();
const rootStore = useRootStore();
const templatesStore = useTemplatesStore();
const workflowsStore = useWorkflowsStore();
const workflowsEEStore = useWorkflowsEEStore();
const uiStore = useUIStore();
@@ -462,10 +444,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
const projectsStore = useProjectsStore();
const tagsStore = useTagsStore();
const toast = useToast();
const message = useMessage();
const i18n = useI18n();
const telemetry = useTelemetry();
const documentTitle = useDocumentTitle();
const setDocumentTitle = (workflowName: string, status: WorkflowTitleStatus) => {
@@ -814,319 +793,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
}
}
function isNodeActivatable(node: INode): boolean {
if (node.disabled) {
return false;
}
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
return (
nodeType !== null &&
nodeType.group.includes('trigger') &&
!NON_ACTIVATABLE_TRIGGER_NODE_TYPES.includes(node.type)
);
}
async function getWorkflowDeactivationInfo(
workflowId: string,
request: IWorkflowDataUpdate,
): Promise<Partial<NotificationOptions> | undefined> {
const missingActivatableTriggerNode =
request.nodes !== undefined && !request.nodes.some(isNodeActivatable);
if (missingActivatableTriggerNode) {
// Automatically deactivate if all activatable triggers are removed
return {
title: i18n.baseText('workflows.deactivated'),
message: i18n.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes'),
type: 'info',
};
}
const conflictData = await checkConflictingWebhooks(workflowId);
if (conflictData) {
// Workflow should not be active if there is live webhook with the same path
return {
title: 'Conflicting Webhook Path',
message: `Workflow set to inactive: Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
type: 'error',
};
}
return undefined;
}
async function saveCurrentWorkflow(
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
redirect = true,
forceSave = false,
): Promise<boolean> {
const readOnlyEnv = useSourceControlStore().preferences.branchReadOnly;
if (readOnlyEnv) {
return false;
}
const isLoading = useCanvasStore().isLoading;
const currentWorkflow = id || (router.currentRoute.value.params.name as string);
const parentFolderId = router.currentRoute.value.query.parentFolderId as string;
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
return await saveAsNewWorkflow({ name, tags, parentFolderId }, redirect);
}
// Workflow exists already so update it
try {
if (!forceSave && isLoading) {
return true;
}
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataUpdate = await getWorkflowDataToSave();
// This can happen if the user has another workflow in the browser history and navigates
// via the browser back button, encountering our warning dialog with the new route already set
if (workflowDataRequest.id !== currentWorkflow) {
throw new Error('Attempted to save a workflow different from the current workflow');
}
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
const deactivateReason = await getWorkflowDeactivationInfo(
currentWorkflow,
workflowDataRequest,
);
if (deactivateReason !== undefined) {
workflowDataRequest.active = false;
if (workflowsStore.isWorkflowActive) {
toast.showMessage(deactivateReason);
workflowsStore.setWorkflowInactive(currentWorkflow);
}
}
const workflowData = await workflowsStore.updateWorkflow(
currentWorkflow,
workflowDataRequest,
forceSave,
);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
if (name) {
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
}
if (tags) {
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.setWorkflowTagIds(tagIds);
}
uiStore.stateIsDirty = false;
uiStore.removeActiveAction('workflowSaving');
void useExternalHooks().run('workflow.afterUpdate', { workflowData });
return true;
} catch (error) {
console.error(error);
uiStore.removeActiveAction('workflowSaving');
if (error.errorCode === 100) {
telemetry.track('User attempted to save locked workflow', {
workflowId: currentWorkflow,
sharing_role: getWorkflowProjectRole(currentWorkflow),
});
const url = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: currentWorkflow },
}).href;
const overwrite = await message.confirm(
i18n.baseText('workflows.concurrentChanges.confirmMessage.message', {
interpolate: {
url,
},
}),
i18n.baseText('workflows.concurrentChanges.confirmMessage.title'),
{
confirmButtonText: i18n.baseText(
'workflows.concurrentChanges.confirmMessage.confirmButtonText',
),
cancelButtonText: i18n.baseText(
'workflows.concurrentChanges.confirmMessage.cancelButtonText',
),
},
);
if (overwrite === MODAL_CONFIRM) {
return await saveCurrentWorkflow({ id, name, tags }, redirect, true);
}
return false;
}
toast.showMessage({
title: i18n.baseText('workflowHelpers.showMessage.title'),
message: error.message,
type: 'error',
});
return false;
}
}
async function saveAsNewWorkflow(
{
name,
tags,
resetWebhookUrls,
resetNodeIds,
openInNewWindow,
parentFolderId,
data,
}: {
name?: string;
tags?: string[];
resetWebhookUrls?: boolean;
openInNewWindow?: boolean;
resetNodeIds?: boolean;
parentFolderId?: string;
data?: IWorkflowDataCreate;
} = {},
redirect = true,
): Promise<boolean> {
try {
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataCreate = data || (await getWorkflowDataToSave());
const changedNodes = {} as IDataObject;
if (resetNodeIds) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
nodeHelpers.assignNodeId(node);
return node;
});
}
if (resetWebhookUrls) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
if (node.webhookId) {
const newId = nodeHelpers.assignWebhookId(node);
node.parameters.path = newId;
changedNodes[node.name] = node.webhookId;
}
return node;
});
}
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
if (parentFolderId) {
workflowDataRequest.parentFolderId = parentFolderId;
}
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
workflowsStore.addWorkflow(workflowData);
if (openInNewWindow) {
const routeData = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
});
window.open(routeData.href, '_blank');
uiStore.removeActiveAction('workflowSaving');
return true;
}
// workflow should not be active if there is live webhook with the same path
if (workflowData.active) {
const conflict = await checkConflictingWebhooks(workflowData.id);
if (conflict) {
workflowData.active = false;
toast.showMessage({
title: 'Conflicting Webhook Path',
message: `Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflict.trigger.name}'.`,
type: 'error',
});
}
}
workflowsStore.setActive(workflowData.active || false);
workflowsStore.setWorkflowId(workflowData.id);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
workflowsStore.setWorkflowSettings((workflowData.settings as IWorkflowSettings) || {});
uiStore.stateIsDirty = false;
Object.keys(changedNodes).forEach((nodeName) => {
const changes = {
key: 'webhookId',
value: changedNodes[nodeName],
name: nodeName,
} as IUpdateInformation;
workflowsStore.setNodeValue(changes);
});
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.setWorkflowTagIds(tagIds);
const templateId = router.currentRoute.value.query.templateId;
if (templateId) {
telemetry.track('User saved new workflow from template', {
template_id: tryToParseNumber(String(templateId)),
workflow_id: workflowData.id,
wf_template_repo_session_id: templatesStore.previousSessionId,
});
}
if (redirect) {
await router.replace({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
query: { action: 'workflowSave' },
});
}
uiStore.removeActiveAction('workflowSaving');
uiStore.stateIsDirty = false;
void useExternalHooks().run('workflow.afterUpdate', { workflowData });
getCurrentWorkflow(true); // refresh cache
return true;
} catch (e) {
uiStore.removeActiveAction('workflowSaving');
toast.showMessage({
title: i18n.baseText('workflowHelpers.showMessage.title'),
message: (e as Error).message,
type: 'error',
});
return false;
}
}
// Updates the position of all the nodes that the top-left node
// is at the given position
function updateNodePositions(
@@ -1292,8 +958,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
getWebhookUrl,
resolveExpression,
updateWorkflow,
saveCurrentWorkflow,
saveAsNewWorkflow,
updateNodePositions,
removeForeignCredentialsFromWorkflow,
getWorkflowProjectRole,

View File

@@ -6,9 +6,14 @@ import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { IWorkflowDataUpdate } from '@/Interface';
import { mockedStore } from '@/__tests__/utils';
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
import { CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
import { nodeTypes } from '@/components/CanvasChat/__test__/data';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
const modalConfirmSpy = vi.fn();
const saveCurrentWorkflowSpy = vi.fn();
vi.mock('@/composables/useMessage', () => {
return {
@@ -18,190 +23,363 @@ vi.mock('@/composables/useMessage', () => {
};
});
vi.mock('@/composables/useWorkflowHelpers', () => {
return {
useWorkflowHelpers: () => ({
saveCurrentWorkflow: saveCurrentWorkflowSpy,
}),
};
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
name: 'Duplicate webhook test',
active: false,
nodes: [
{
parameters: {
path: '5340ae49-2c96-4492-9073-7744d2e52b8a',
options: {},
},
id: 'c1e1b6e7-df13-41b1-95f6-42903b85e438',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [680, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
{
parameters: {
path: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
options: {},
},
id: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
name: 'Webhook 2',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [700, 40],
webhookId: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
},
{
parameters: {
resume: 'webhook',
options: {
webhookSuffix: '/test',
},
},
id: '979d8443-51b1-48e2-b239-acf399b66509',
name: 'Wait',
type: 'n8n-nodes-base.wait',
typeVersion: 1.1,
position: [900, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
],
connections: {},
});
describe('promptSaveUnsavedWorkflowChanges', () => {
beforeAll(() => {
setActivePinia(createTestingPinia());
});
describe('useWorkflowSaving', () => {
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
afterEach(() => {
vi.clearAllMocks();
});
beforeEach(() => {
vi.resetAllMocks();
setActivePinia(createTestingPinia({ stubActions: false }));
workflowsStore = mockedStore(useWorkflowsStore);
nodeTypesStore = mockedStore(useNodeTypesStore);
nodeTypesStore.setNodeTypes(nodeTypes);
});
it('should prompt the user to save changes and proceed if confirmed', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn().mockResolvedValue(true);
const cancel = vi.fn();
describe('promptSaveUnsavedWorkflowChanges', () => {
it('should prompt the user to save changes and proceed if confirmed', async () => {
const workflow = createTestWorkflow({
id: 'w0',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
active: true,
});
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
const npsSurveyStore = useNpsSurveyStore();
vi.spyOn(npsSurveyStore, 'fetchPromptsData').mockResolvedValue();
workflowsStore.setWorkflow(workflow);
saveCurrentWorkflowSpy.mockResolvedValue(true);
const next = vi.fn();
const confirm = vi.fn().mockResolvedValue(true);
const cancel = vi.fn();
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
const npsSurveyStore = useNpsSurveyStore();
vi.spyOn(npsSurveyStore, 'fetchPromptsData').mockResolvedValue();
expect(modalConfirmSpy).toHaveBeenCalled();
expect(npsSurveyStore.fetchPromptsData).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
expect(uiStore.stateIsDirty).toEqual(false);
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
expect(confirm).toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(true);
expect(cancel).not.toHaveBeenCalled();
const resolveSpy = vi.fn();
const resolveMarker = Symbol();
resolveSpy.mockReturnValue(resolveMarker);
const mockRouter = {
resolve: resolveSpy,
currentRoute: { value: { params: { name: workflow.id }, query: { parentFolderId: '' } } },
};
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({
router: mockRouter as never,
});
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(npsSurveyStore.fetchPromptsData).toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(false);
expect(confirm).toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(true);
expect(cancel).not.toHaveBeenCalled();
});
it('should not proceed if the user cancels the confirmation modal', async () => {
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CANCEL);
const workflowSaving = useWorkflowSaving({ router });
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(false);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalled();
expect(next).toHaveBeenCalledWith();
});
it('should restore the route if the modal is closed and the workflow is not new', async () => {
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
const workflowStore = useWorkflowsStore();
const MOCK_ID = 'existing-workflow-id';
workflowStore.workflow.id = MOCK_ID;
// Mock message.confirm
modalConfirmSpy.mockResolvedValue('close');
const workflowSaving = useWorkflowSaving({ router });
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(true);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(
router.resolve({
name: VIEWS.WORKFLOW,
params: { name: MOCK_ID },
}),
);
});
it('should close modal if workflow is not new', async () => {
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
const workflowStore = useWorkflowsStore();
workflowStore.workflow.id = PLACEHOLDER_EMPTY_WORKFLOW_ID;
// Mock message.confirm
modalConfirmSpy.mockResolvedValue('close');
const workflowSaving = useWorkflowSaving({ router });
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(true);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).not.toHaveBeenCalled();
});
it('should proceed without prompting if there are no unsaved changes', async () => {
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = false;
const workflowSaving = useWorkflowSaving({ router });
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).not.toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(false);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith();
});
it('should handle save failure and restore the route', async () => {
const workflow = createTestWorkflow({
id: 'w0',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
active: true,
});
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.setWorkflow(workflow);
const updateWorkflowSpy = vi.spyOn(workflowsStore, 'updateWorkflow');
updateWorkflowSpy.mockImplementation(() => {
throw new Error();
});
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
const resolveSpy = vi.fn();
const resolveMarker = Symbol();
resolveSpy.mockReturnValue(resolveMarker);
const mockRouter = {
resolve: resolveSpy,
currentRoute: { value: { params: { name: workflow.id }, query: { parentFolderId: '' } } },
};
const workflowSaving = useWorkflowSaving({ router: mockRouter as never });
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(updateWorkflowSpy).toBeCalled();
expect(uiStore.stateIsDirty).toEqual(true);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(resolveMarker);
});
});
describe('saveAsNewWorkflow', () => {
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowSaving({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
it('should not proceed if the user cancels the confirmation modal', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: false,
data: workflow,
});
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Expect webhookIds and paths to be the same as in the original workflow
expect(webHookIdsPreSave).toEqual(webHookIdsPostSave);
expect(pathsPreSave).toEqual(pathsPostSave);
});
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CANCEL);
it('should respect `resetWebhookUrls: true` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowSaving({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: true,
data: workflow,
});
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(false);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalled();
expect(next).toHaveBeenCalledWith();
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Now, expect webhookIds and paths to be different
expect(webHookIdsPreSave).not.toEqual(webHookIdsPostSave);
expect(pathsPreSave).not.toEqual(pathsPostSave);
});
});
describe('saveCurrentWorkflow', () => {
it('should save the current workflow', async () => {
const workflow = createTestWorkflow({
id: 'w0',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
active: true,
});
it('should restore the route if the modal is closed and the workflow is not new', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
workflowsStore.setWorkflow(workflow);
const workflowStore = useWorkflowsStore();
const MOCK_ID = 'existing-workflow-id';
workflowStore.workflow.id = MOCK_ID;
const { saveCurrentWorkflow } = useWorkflowSaving({ router });
await saveCurrentWorkflow({ id: 'w0' });
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
'w0',
expect.objectContaining({ id: 'w0', active: true }),
false,
);
});
// Mock message.confirm
modalConfirmSpy.mockResolvedValue('close');
it('should include active=false in the request if the workflow has no activatable trigger node', async () => {
const workflow = createTestWorkflow({
id: 'w1',
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true })],
active: true,
});
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(true);
workflowsStore.setWorkflow(workflow);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(
router.resolve({
name: VIEWS.WORKFLOW,
params: { name: MOCK_ID },
}),
);
});
it('should close modal if workflow is not new', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
const workflowStore = useWorkflowsStore();
workflowStore.workflow.id = PLACEHOLDER_EMPTY_WORKFLOW_ID;
// Mock message.confirm
modalConfirmSpy.mockResolvedValue('close');
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(true);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).not.toHaveBeenCalled();
});
it('should proceed without prompting if there are no unsaved changes', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = false;
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).not.toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
expect(uiStore.stateIsDirty).toEqual(false);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith();
});
it('should handle save failure and restore the route', async () => {
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
const next = vi.fn();
const confirm = vi.fn();
const cancel = vi.fn();
// Mock state
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
const workflowStore = useWorkflowsStore();
const MOCK_ID = 'existing-workflow-id';
workflowStore.workflow.id = MOCK_ID;
saveCurrentWorkflowSpy.mockResolvedValue(false);
// Mock message.confirm
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
expect(modalConfirmSpy).toHaveBeenCalled();
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
expect(uiStore.stateIsDirty).toEqual(true);
expect(confirm).not.toHaveBeenCalled();
expect(cancel).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(
router.resolve({
name: VIEWS.WORKFLOW,
params: { name: MOCK_ID },
}),
);
const { saveCurrentWorkflow } = useWorkflowSaving({ router });
await saveCurrentWorkflow({ id: 'w1' });
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
'w1',
expect.objectContaining({ id: 'w1', active: false }),
false,
);
expect(workflowsStore.setWorkflowInactive).toHaveBeenCalled();
});
});
});

View File

@@ -7,11 +7,29 @@ import {
MODAL_CANCEL,
MODAL_CLOSE,
MODAL_CONFIRM,
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
VIEWS,
} from '@/constants';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useCanvasStore } from '@/stores/canvas.store';
import type {
ITag,
IUpdateInformation,
IWorkflowDataCreate,
IWorkflowDataUpdate,
NotificationOptions,
} from '@/Interface';
import type { IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useToast } from './useToast';
import { useExternalHooks } from './useExternalHooks';
import { useTelemetry } from './useTelemetry';
import { useNodeHelpers } from './useNodeHelpers';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useTemplatesStore } from '@/stores/templates.store';
export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRouter> }) {
const uiStore = useUIStore();
@@ -19,8 +37,18 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
const message = useMessage();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const toast = useToast();
const telemetry = useTelemetry();
const nodeHelpers = useNodeHelpers();
const templatesStore = useTemplatesStore();
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
const {
getWorkflowDataToSave,
checkConflictingWebhooks,
getWorkflowProjectRole,
getCurrentWorkflow,
} = useWorkflowHelpers();
async function promptSaveUnsavedWorkflowChanges(
next: NavigationGuardNext,
@@ -51,6 +79,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
switch (response) {
case MODAL_CONFIRM:
const saved = await saveCurrentWorkflow({}, false);
if (saved) {
await npsSurveyStore.fetchPromptsData();
uiStore.stateIsDirty = false;
@@ -89,7 +118,320 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
);
}
function isNodeActivatable(node: INode): boolean {
if (node.disabled) {
return false;
}
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
return (
nodeType !== null &&
nodeType.group.includes('trigger') &&
!NON_ACTIVATABLE_TRIGGER_NODE_TYPES.includes(node.type)
);
}
async function getWorkflowDeactivationInfo(
workflowId: string,
request: IWorkflowDataUpdate,
): Promise<Partial<NotificationOptions> | undefined> {
const missingActivatableTriggerNode =
request.nodes !== undefined && !request.nodes.some(isNodeActivatable);
if (missingActivatableTriggerNode) {
// Automatically deactivate if all activatable triggers are removed
return {
title: i18n.baseText('workflows.deactivated'),
message: i18n.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes'),
type: 'info',
};
}
const conflictData = await checkConflictingWebhooks(workflowId);
if (conflictData) {
// Workflow should not be active if there is live webhook with the same path
return {
title: 'Conflicting Webhook Path',
message: `Workflow set to inactive: Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
type: 'error',
};
}
return undefined;
}
async function saveCurrentWorkflow(
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
redirect = true,
forceSave = false,
): Promise<boolean> {
const readOnlyEnv = useSourceControlStore().preferences.branchReadOnly;
if (readOnlyEnv) {
return false;
}
const isLoading = useCanvasStore().isLoading;
const currentWorkflow = id || (router.currentRoute.value.params.name as string);
const parentFolderId = router.currentRoute.value.query.parentFolderId as string;
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
return await saveAsNewWorkflow({ name, tags, parentFolderId }, redirect);
}
// Workflow exists already so update it
try {
if (!forceSave && isLoading) {
return true;
}
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataUpdate = await getWorkflowDataToSave();
// This can happen if the user has another workflow in the browser history and navigates
// via the browser back button, encountering our warning dialog with the new route already set
if (workflowDataRequest.id !== currentWorkflow) {
throw new Error('Attempted to save a workflow different from the current workflow');
}
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
const deactivateReason = await getWorkflowDeactivationInfo(
currentWorkflow,
workflowDataRequest,
);
if (deactivateReason !== undefined) {
workflowDataRequest.active = false;
if (workflowsStore.isWorkflowActive) {
toast.showMessage(deactivateReason);
workflowsStore.setWorkflowInactive(currentWorkflow);
}
}
const workflowData = await workflowsStore.updateWorkflow(
currentWorkflow,
workflowDataRequest,
forceSave,
);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
if (name) {
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
}
if (tags) {
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.setWorkflowTagIds(tagIds);
}
uiStore.stateIsDirty = false;
uiStore.removeActiveAction('workflowSaving');
void useExternalHooks().run('workflow.afterUpdate', { workflowData });
return true;
} catch (error) {
console.error(error);
uiStore.removeActiveAction('workflowSaving');
if (error.errorCode === 100) {
telemetry.track('User attempted to save locked workflow', {
workflowId: currentWorkflow,
sharing_role: getWorkflowProjectRole(currentWorkflow),
});
const url = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: currentWorkflow },
}).href;
const overwrite = await message.confirm(
i18n.baseText('workflows.concurrentChanges.confirmMessage.message', {
interpolate: {
url,
},
}),
i18n.baseText('workflows.concurrentChanges.confirmMessage.title'),
{
confirmButtonText: i18n.baseText(
'workflows.concurrentChanges.confirmMessage.confirmButtonText',
),
cancelButtonText: i18n.baseText(
'workflows.concurrentChanges.confirmMessage.cancelButtonText',
),
},
);
if (overwrite === MODAL_CONFIRM) {
return await saveCurrentWorkflow({ id, name, tags }, redirect, true);
}
return false;
}
toast.showMessage({
title: i18n.baseText('workflowHelpers.showMessage.title'),
message: error.message,
type: 'error',
});
return false;
}
}
async function saveAsNewWorkflow(
{
name,
tags,
resetWebhookUrls,
resetNodeIds,
openInNewWindow,
parentFolderId,
data,
}: {
name?: string;
tags?: string[];
resetWebhookUrls?: boolean;
openInNewWindow?: boolean;
resetNodeIds?: boolean;
parentFolderId?: string;
data?: IWorkflowDataCreate;
} = {},
redirect = true,
): Promise<boolean> {
try {
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataCreate = data || (await getWorkflowDataToSave());
const changedNodes = {} as IDataObject;
if (resetNodeIds) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
nodeHelpers.assignNodeId(node);
return node;
});
}
if (resetWebhookUrls) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
if (node.webhookId) {
const newId = nodeHelpers.assignWebhookId(node);
node.parameters.path = newId;
changedNodes[node.name] = node.webhookId;
}
return node;
});
}
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
if (parentFolderId) {
workflowDataRequest.parentFolderId = parentFolderId;
}
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
workflowsStore.addWorkflow(workflowData);
if (openInNewWindow) {
const routeData = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
});
window.open(routeData.href, '_blank');
uiStore.removeActiveAction('workflowSaving');
return true;
}
// workflow should not be active if there is live webhook with the same path
if (workflowData.active) {
const conflict = await checkConflictingWebhooks(workflowData.id);
if (conflict) {
workflowData.active = false;
toast.showMessage({
title: 'Conflicting Webhook Path',
message: `Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflict.trigger.name}'.`,
type: 'error',
});
}
}
workflowsStore.setActive(workflowData.active || false);
workflowsStore.setWorkflowId(workflowData.id);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
workflowsStore.setWorkflowSettings((workflowData.settings as IWorkflowSettings) || {});
uiStore.stateIsDirty = false;
Object.keys(changedNodes).forEach((nodeName) => {
const changes = {
key: 'webhookId',
value: changedNodes[nodeName],
name: nodeName,
} as IUpdateInformation;
workflowsStore.setNodeValue(changes);
});
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.setWorkflowTagIds(tagIds);
const templateId = router.currentRoute.value.query.templateId;
if (templateId) {
telemetry.track('User saved new workflow from template', {
template_id: tryToParseNumber(String(templateId)),
workflow_id: workflowData.id,
wf_template_repo_session_id: templatesStore.previousSessionId,
});
}
if (redirect) {
await router.replace({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
query: { action: 'workflowSave' },
});
}
uiStore.removeActiveAction('workflowSaving');
uiStore.stateIsDirty = false;
void useExternalHooks().run('workflow.afterUpdate', { workflowData });
getCurrentWorkflow(true); // refresh cache
return true;
} catch (e) {
uiStore.removeActiveAction('workflowSaving');
toast.showMessage({
title: i18n.baseText('workflowHelpers.showMessage.title'),
message: (e as Error).message,
type: 'error',
});
return false;
}
}
return {
promptSaveUnsavedWorkflowChanges,
saveCurrentWorkflow,
saveAsNewWorkflow,
};
}

View File

@@ -17,7 +17,6 @@ import {
import type { EditorView } from '@codemirror/view';
import { EditorSelection, type TransactionSpec } from '@codemirror/state';
import type { SyntaxNode, Tree } from '@lezer/common';
import { useRouter } from 'vue-router';
import type { DocMetadata } from 'n8n-workflow';
import { escapeMappingString } from '@/utils/mappingUtils';
@@ -210,7 +209,7 @@ export function autocompletableNodeNames() {
const activeNodeName = activeNode.name;
const workflow = useWorkflowHelpers({ router: useRouter() }).getCurrentWorkflow();
const workflow = useWorkflowHelpers().getCurrentWorkflow();
const nonMainChildren = workflow.getChildNodes(activeNodeName, 'ALL_NON_MAIN');
// This is a tool node, look for the nearest node with main connections
@@ -222,7 +221,7 @@ export function autocompletableNodeNames() {
}
export function getPreviousNodes(nodeName: string) {
const workflow = useWorkflowHelpers({ router: useRouter() }).getCurrentWorkflow();
const workflow = useWorkflowHelpers().getCurrentWorkflow();
return workflow
.getParentNodesByDepth(nodeName)
.map((node) => node.name)

View File

@@ -88,7 +88,6 @@ import type { ProjectSharingData } from '@/types/projects.types';
import type { PushPayload } from '@n8n/api-types';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useSettingsStore } from './settings.store';
import { clearPopupWindowState, openFormPopupWindow } from '@/utils/executionUtils';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
@@ -126,8 +125,7 @@ let cachedWorkflow: Workflow | null = null;
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const telemetry = useTelemetry();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const nodeHelpers = useNodeHelpers();

View File

@@ -7,7 +7,6 @@ import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n';
import { useRouter } from 'vue-router';
import { useEvaluationStore } from '@/stores/evaluation.store.ee';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -25,11 +24,10 @@ const usageStore = useUsageStore();
const evaluationStore = useEvaluationStore();
const nodeTypesStore = useNodeTypesStore();
const telemetry = useTelemetry();
const router = useRouter();
const toast = useToast();
const locale = useI18n();
const { initializeWorkspace } = useCanvasOperations({ router });
const { initializeWorkspace } = useCanvasOperations();
const evaluationsLicensed = computed(() => {
return usageStore.workflowsWithEvaluationsLimit !== 0;

View File

@@ -161,7 +161,8 @@ const externalHooks = useExternalHooks();
const toast = useToast();
const message = useMessage();
const documentTitle = useDocumentTitle();
const workflowHelpers = useWorkflowHelpers({ router });
const workflowHelpers = useWorkflowHelpers();
const workflowSaving = useWorkflowSaving({ router });
const nodeHelpers = useNodeHelpers();
const nodeTypesStore = useNodeTypesStore();
@@ -237,7 +238,7 @@ const {
editableWorkflowObject,
lastClickPosition,
startChat,
} = useCanvasOperations({ router });
} = useCanvasOperations();
const { extractWorkflow } = useWorkflowExtraction();
const { applyExecutionData } = useExecutionDebugging();
useClipboard({ onPaste: onClipboardPaste });
@@ -818,7 +819,7 @@ async function onSaveWorkflow() {
if (workflowIsSaved || workflowIsArchived) {
return;
}
const saved = await workflowHelpers.saveCurrentWorkflow();
const saved = await workflowSaving.saveCurrentWorkflow();
if (saved) {
canvasEventBus.emit('saved:workflow');
}

View File

@@ -26,7 +26,7 @@ const router = useRouter();
const toast = useToast();
const { callDebounced } = useDebounce();
const { initializeWorkspace } = useCanvasOperations({ router });
const { initializeWorkspace } = useCanvasOperations();
const loading = ref(false);
const loadingMore = ref(false);