diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts index 1de63e9169..36ad56729e 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts @@ -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)); diff --git a/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue index d05ac1c70d..929984bca0 100644 --- a/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -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 => { ); } - const saved = await workflowHelpers.saveAsNewWorkflow({ + const saved = await workflowSaving.saveAsNewWorkflow({ name: workflowName, data: workflowToUpdate, tags: currentTagIds.value, diff --git a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue index e9eb452963..08f776709d 100644 --- a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -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'); diff --git a/packages/frontend/editor-ui/src/components/NodeWebhooks.vue b/packages/frontend/editor-ui/src/components/NodeWebhooks.vue index fa21d35162..368cb2be3a 100644 --- a/packages/frontend/editor-ui/src/components/NodeWebhooks.vue +++ b/packages/frontend/editor-ui/src/components/NodeWebhooks.vue @@ -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(); diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index c23a4a7fad..778c369a3d 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -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(); diff --git a/packages/frontend/editor-ui/src/components/ParameterInputList.vue b/packages/frontend/editor-ui/src/components/ParameterInputList.vue index 656efb1468..9abcf9ab0d 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInputList.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInputList.vue @@ -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); diff --git a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index 17f71b4803..fb79d88139 100644 --- a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -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(); diff --git a/packages/frontend/editor-ui/src/components/SQLEditor.test.ts b/packages/frontend/editor-ui/src/components/SQLEditor.test.ts index 49bf62d62a..f4e9f81154 100644 --- a/packages/frontend/editor-ui/src/components/SQLEditor.test.ts +++ b/packages/frontend/editor-ui/src/components/SQLEditor.test.ts @@ -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, }); diff --git a/packages/frontend/editor-ui/src/components/TriggerPanel.vue b/packages/frontend/editor-ui/src/components/TriggerPanel.vue index 040ef94b5d..357b3a40a5 100644 --- a/packages/frontend/editor-ui/src/components/TriggerPanel.vue +++ b/packages/frontend/editor-ui/src/components/TriggerPanel.vue @@ -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(); diff --git a/packages/frontend/editor-ui/src/components/WorkflowActivator.vue b/packages/frontend/editor-ui/src/components/WorkflowActivator.vue index 9600c84d0d..dd2dbc7199 100644 --- a/packages/frontend/editor-ui/src/components/WorkflowActivator.vue +++ b/packages/frontend/editor-ui/src/components/WorkflowActivator.vue @@ -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(); diff --git a/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts b/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts index 37d8918c92..b84a900c28 100644 --- a/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts +++ b/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts @@ -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 { const settingsStore = useSettingsStore(); @@ -15,8 +14,7 @@ export function useNodeSettingsInCanvas(): ComputedRef { 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); diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue index d2c2b5ea6d..16d072506c 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue @@ -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); diff --git a/packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsInfoAccordion.vue b/packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsInfoAccordion.vue index 8c6bfd5040..d7a332bb63 100644 --- a/packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsInfoAccordion.vue +++ b/packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsInfoAccordion.vue @@ -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 { if (!currentId) { return; } - const saved = await workflowHelpers.saveCurrentWorkflow({ + const saved = await workflowSaving.saveCurrentWorkflow({ id: currentId, name: workflowName.value, tags: currentWorkflowTagIds.value, diff --git a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.ts b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.ts index 1bdb30f063..79c41d8404 100644 --- a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.ts @@ -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(); /** diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts index fe954b6c77..7a1a26605c 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts @@ -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({ 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({ 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({ webhookId: undefined }); - const { resolveNodeWebhook } = useCanvasOperations({ router }); + const { resolveNodeWebhook } = useCanvasOperations(); resolveNodeWebhook(node, mock({ 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(['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(); - 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(['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(['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(['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'; diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts index 699005c65a..3d1b7e078b 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts @@ -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 }) { +export function useCanvasOperations() { const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); const credentialsStore = useCredentialsStore(); @@ -158,7 +157,7 @@ export function useCanvasOperations({ router }: { router: ReturnType 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(() => diff --git a/packages/frontend/editor-ui/src/composables/useExpressionEditor.test.ts b/packages/frontend/editor-ui/src/composables/useExpressionEditor.test.ts index 7f5b6d73c1..de0e72c77d 100644 --- a/packages/frontend/editor-ui/src/composables/useExpressionEditor.test.ts +++ b/packages/frontend/editor-ui/src/composables/useExpressionEditor.test.ts @@ -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, }); diff --git a/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts b/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts index 5b1d456c52..53508a91f9 100644 --- a/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts +++ b/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts @@ -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(); const hasFocus = ref(false); diff --git a/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts b/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts index f3806d468a..84069eb5fa 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts @@ -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; @@ -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); diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts index 4b03cd8362..9b9fdf9b90 100644 --- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts +++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts @@ -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 }, ) { 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 }, -) { +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 }, -) { +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(); diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionRecovered.ts b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionRecovered.ts index acb20d01fe..e4ba939f94 100644 --- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionRecovered.ts +++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionRecovered.ts @@ -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); diff --git a/packages/frontend/editor-ui/src/composables/useResolvedExpression.test.ts b/packages/frontend/editor-ui/src/composables/useResolvedExpression.test.ts index 723bb6bbb4..f31e40b46e 100644 --- a/packages/frontend/editor-ui/src/composables/useResolvedExpression.test.ts +++ b/packages/frontend/editor-ui/src/composables/useResolvedExpression.test.ts @@ -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 { const mock = vi.fn(); vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({ - ...workflowHelpers.useWorkflowHelpers({ router: useRouter() }), + ...workflowHelpers.useWorkflowHelpers(), resolveExpression: mock, }); diff --git a/packages/frontend/editor-ui/src/composables/useResolvedExpression.ts b/packages/frontend/editor-ui/src/composables/useResolvedExpression.ts index 88a799ced1..9797e9e195 100644 --- a/packages/frontend/editor-ui/src/composables/useResolvedExpression.ts +++ b/packages/frontend/editor-ui/src/composables/useResolvedExpression.ts @@ -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(null); const resolvedExpressionString = ref(''); diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts index 6936ebdce9..4dd5bf8a1e 100644 --- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts @@ -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', diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts index e8a01d0c15..ad56250a5b 100644 --- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts @@ -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 }) { 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 { @@ -144,7 +146,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType ({ - 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>; let workflowsEEStore: ReturnType; let tagsStore: ReturnType; let uiStore: ReturnType; - let nodeTypesStore: ReturnType>; 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(); - }); - }); }); diff --git a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts index 0ed15e9bda..ba9a2d32ea 100644 --- a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts @@ -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 }) { - 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 { @@ -814,319 +793,6 @@ export function useWorkflowHelpers(options: { router: ReturnType | 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 { - 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 { - 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 { 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>; + let nodeTypesStore: ReturnType>; + 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(); + }); }); }); diff --git a/packages/frontend/editor-ui/src/composables/useWorkflowSaving.ts b/packages/frontend/editor-ui/src/composables/useWorkflowSaving.ts index 4640f1ee7d..4aded34351 100644 --- a/packages/frontend/editor-ui/src/composables/useWorkflowSaving.ts +++ b/packages/frontend/editor-ui/src/composables/useWorkflowSaving.ts @@ -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 }) { const uiStore = useUIStore(); @@ -19,8 +37,18 @@ export function useWorkflowSaving({ router }: { router: ReturnType | 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 { + 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 { + 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, }; } diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts index 510f3f5dad..5a4e82db61 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts @@ -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) diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.ts b/packages/frontend/editor-ui/src/stores/workflows.store.ts index d442afe1dd..1fc81b186f 100644 --- a/packages/frontend/editor-ui/src/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/stores/workflows.store.ts @@ -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(); diff --git a/packages/frontend/editor-ui/src/views/Evaluations.ee/EvaluationsRootView.vue b/packages/frontend/editor-ui/src/views/Evaluations.ee/EvaluationsRootView.vue index 2a58562467..b04757a025 100644 --- a/packages/frontend/editor-ui/src/views/Evaluations.ee/EvaluationsRootView.vue +++ b/packages/frontend/editor-ui/src/views/Evaluations.ee/EvaluationsRootView.vue @@ -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; diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue index 995363ee13..673ee986d9 100644 --- a/packages/frontend/editor-ui/src/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/views/NodeView.vue @@ -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'); } diff --git a/packages/frontend/editor-ui/src/views/WorkflowExecutionsView.vue b/packages/frontend/editor-ui/src/views/WorkflowExecutionsView.vue index ff8a4e809a..6e71d439cd 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowExecutionsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowExecutionsView.vue @@ -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);