diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index 20469d0225..1eaaf2f8ce 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -71,7 +71,7 @@ export default defineComponent({ props: ['modalName', 'isActive', 'data'], setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { ...useToast(), diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue index 2b1cccc151..53dd7a6215 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue @@ -64,7 +64,7 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue index e8c4d15bb5..281b642984 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue @@ -84,7 +84,7 @@ export default defineComponent({ setup() { const externalHooks = useExternalHooks(); const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); const { callDebounced } = useDebounce(); return { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 6958642c8a..8658b685e3 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -217,7 +217,7 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 2f02a01675..2c25da76bf 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -209,7 +209,7 @@ export default defineComponent({ const { activeNode } = storeToRefs(ndvStore); const pinnedData = usePinnedData(activeNode); const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { externalHooks, diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index 5323ac4d87..d7e41f79e3 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -34,7 +34,6 @@ import { } from '@/constants'; import type { INodeUi } from '@/Interface'; import type { INodeTypeDescription } from 'n8n-workflow'; -import { workflowRun } from '@/mixins/workflowRun'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -43,9 +42,11 @@ import { useToast } from '@/composables/useToast'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { nodeViewEventBus } from '@/event-bus'; import { usePinnedData } from '@/composables/usePinnedData'; +import { useRunWorkflow } from '@/composables/useRunWorkflow'; +import { useUIStore } from '@/stores/ui.store'; +import { useRouter } from 'vue-router'; export default defineComponent({ - mixins: [workflowRun], inheritAttrs: false, props: { nodeName: { @@ -76,23 +77,24 @@ export default defineComponent({ type: Boolean, }, }, - setup(props, ctx) { + setup(props) { + const router = useRouter(); const workflowsStore = useWorkflowsStore(); const node = workflowsStore.getNodeByName(props.nodeName); const pinnedData = usePinnedData(node); const externalHooks = useExternalHooks(); + const { runWorkflow } = useRunWorkflow({ router }); return { externalHooks, pinnedData, + runWorkflow, ...useToast(), ...useMessage(), - // eslint-disable-next-line @typescript-eslint/no-misused-promises - ...workflowRun.setup?.(props, ctx), }; }, computed: { - ...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore), + ...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore, useUIStore), node(): INodeUi | null { return this.workflowsStore.getNodeByName(this.nodeName); }, diff --git a/packages/editor-ui/src/components/NodeWebhooks.vue b/packages/editor-ui/src/components/NodeWebhooks.vue index ecdd0857f1..7f107f81dd 100644 --- a/packages/editor-ui/src/components/NodeWebhooks.vue +++ b/packages/editor-ui/src/components/NodeWebhooks.vue @@ -75,7 +75,7 @@ export default defineComponent({ setup() { const router = useRouter(); const clipboard = useClipboard(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { clipboard, workflowHelpers, diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 84d7f16225..90d7f5d8fe 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -619,7 +619,7 @@ export default defineComponent({ const nodeHelpers = useNodeHelpers(); const { callDebounced } = useDebounce(); const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { externalHooks, diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 5a05ac12ad..55bd546c3d 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -251,7 +251,7 @@ export default defineComponent({ const nodeHelpers = useNodeHelpers(); const asyncLoadingError = ref(false); const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); // This will catch errors in async components onErrorCaptured((e, component) => { diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index d5e3f68ddd..84216baf6f 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -151,7 +151,7 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index a5fa01eae7..b58db18d10 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -259,7 +259,7 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); const { callDebounced } = useDebounce(); diff --git a/packages/editor-ui/src/components/TriggerPanel.vue b/packages/editor-ui/src/components/TriggerPanel.vue index 0fa78c109c..d668e15fe6 100644 --- a/packages/editor-ui/src/components/TriggerPanel.vue +++ b/packages/editor-ui/src/components/TriggerPanel.vue @@ -148,7 +148,7 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index aa18ea1d27..09a8c84ff4 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -63,7 +63,7 @@ export default defineComponent({ props: ['path', 'redactValues'], setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, diff --git a/packages/editor-ui/src/components/WorkflowLMChat.vue b/packages/editor-ui/src/components/WorkflowLMChat.vue index 054e9e7f66..7bffa1cae8 100644 --- a/packages/editor-ui/src/components/WorkflowLMChat.vue +++ b/packages/editor-ui/src/components/WorkflowLMChat.vue @@ -135,7 +135,6 @@ import { WORKFLOW_LM_CHAT_MODAL_KEY, } from '@/constants'; -import { workflowRun } from '@/mixins/workflowRun'; import { get, last } from 'lodash-es'; import { useUIStore } from '@/stores/ui.store'; @@ -152,6 +151,7 @@ import MessageTyping from '@n8n/chat/components/MessageTyping.vue'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useRouter } from 'vue-router'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import { useRunWorkflow } from '@/composables/useRunWorkflow'; const RunDataAi = defineAsyncComponent( async () => await import('@/components/RunDataAi/RunDataAi.vue'), @@ -181,18 +181,17 @@ export default defineComponent({ MessageTyping, RunDataAi, }, - mixins: [workflowRun], - setup(props, ctx) { + setup() { const router = useRouter(); const externalHooks = useExternalHooks(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); + const { runWorkflow } = useRunWorkflow({ router }); return { + runWorkflow, externalHooks, workflowHelpers, ...useToast(), - // eslint-disable-next-line @typescript-eslint/no-misused-promises - ...workflowRun.setup?.(props, ctx), }; }, data() { @@ -350,7 +349,7 @@ export default defineComponent({ if (!memoryConnection) return []; - const nodeResultData = this.workflowsStore?.getWorkflowResultDataByNodeName( + const nodeResultData = this.workflowsStore.getWorkflowResultDataByNodeName( memoryConnection.node, ); diff --git a/packages/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/editor-ui/src/composables/useRunWorkflow.test.ts new file mode 100644 index 0000000000..2908e50820 --- /dev/null +++ b/packages/editor-ui/src/composables/useRunWorkflow.test.ts @@ -0,0 +1,289 @@ +import { useRootStore } from '@/stores/n8nRoot.store'; +import { useRunWorkflow } from '@/composables/useRunWorkflow'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; +import type { IStartRunData, IWorkflowData } from '@/Interface'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useUIStore } from '@/stores/ui.store'; +import { useToast } from '@/composables/useToast'; +import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; +import { useRouter } from 'vue-router'; +import type { IPinData, IRunData, Workflow } from 'n8n-workflow'; + +vi.mock('@/stores/n8nRoot.store', () => ({ + useRootStore: vi.fn().mockReturnValue({ pushConnectionActive: true }), +})); + +vi.mock('@/stores/workflows.store', () => ({ + useWorkflowsStore: vi.fn().mockReturnValue({ + runWorkflow: vi.fn(), + subWorkflowExecutionError: null, + getWorkflowRunData: null, + setWorkflowExecutionData: vi.fn(), + activeExecutionId: null, + nodesIssuesExist: false, + executionWaitingForWebhook: false, + getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }), + getNodeByName: vi.fn(), + }), +})); + +vi.mock('@/stores/ui.store', () => ({ + useUIStore: vi.fn().mockReturnValue({ + isActionActive: vi.fn().mockReturnValue(false), + addActiveAction: vi.fn(), + removeActiveAction: vi.fn(), + }), +})); + +vi.mock('@/composables/useTelemetry', () => ({ + useTelemetry: vi.fn().mockReturnValue({ track: vi.fn() }), +})); + +vi.mock('@/composables/useI18n', () => ({ + useI18n: vi.fn().mockReturnValue({ baseText: vi.fn().mockImplementation((key) => key) }), +})); + +vi.mock('@/composables/useExternalHooks', () => ({ + useExternalHooks: vi.fn().mockReturnValue({ + run: vi.fn(), + }), +})); + +vi.mock('@/composables/useToast', () => ({ + useToast: vi.fn().mockReturnValue({ + clearAllStickyNotifications: vi.fn(), + showMessage: vi.fn(), + showError: vi.fn(), + }), +})); + +vi.mock('@/composables/useWorkflowHelpers', () => ({ + useWorkflowHelpers: vi.fn().mockReturnValue({ + getCurrentWorkflow: vi.fn(), + checkReadyForExecution: vi.fn(), + saveCurrentWorkflow: vi.fn(), + getWorkflowDataToSave: vi.fn(), + }), +})); + +vi.mock('@/composables/useNodeHelpers', () => ({ + useNodeHelpers: vi.fn().mockReturnValue({ + refreshNodeIssues: vi.fn(), + updateNodesExecutionIssues: vi.fn(), + }), +})); + +vi.mock('@/composables/useTitleChange', () => ({ + useTitleChange: vi.fn().mockReturnValue({ titleSet: vi.fn() }), +})); + +vi.mock('vue-router', async (importOriginal) => { + const { RouterLink } = await importOriginal(); + return { + RouterLink, + useRouter: vi.fn().mockReturnValue({ + push: vi.fn(), + }), + }; +}); + +describe('useRunWorkflow({ router })', () => { + let rootStore: ReturnType; + let uiStore: ReturnType; + let workflowsStore: ReturnType; + let router: ReturnType; + let toast: ReturnType; + let workflowHelpers: ReturnType; + let nodeHelpers: ReturnType; + + beforeAll(() => { + const pinia = createTestingPinia(); + + setActivePinia(pinia); + + rootStore = useRootStore(); + uiStore = useUIStore(); + workflowsStore = useWorkflowsStore(); + + router = useRouter(); + toast = useToast(); + workflowHelpers = useWorkflowHelpers({ router }); + nodeHelpers = useNodeHelpers(); + }); + + describe('runWorkflowApi()', () => { + it('should throw an error if push connection is not active', async () => { + const { runWorkflowApi } = useRunWorkflow({ router }); + rootStore.pushConnectionActive = false; + + await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow( + 'workflowRun.noActiveConnectionToTheServer', + ); + }); + + it('should successfully run a workflow', async () => { + const { runWorkflowApi } = useRunWorkflow({ router }); + rootStore.pushConnectionActive = true; + + const mockResponse = { executionId: '123', waitingForWebhook: false }; + vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse); + + const response = await runWorkflowApi({} as IStartRunData); + + expect(response).toEqual(mockResponse); + expect(workflowsStore.activeExecutionId).toBe('123'); + expect(workflowsStore.executionWaitingForWebhook).toBe(false); + expect(uiStore.addActiveAction).toHaveBeenCalledWith('workflowRunning'); + }); + + it('should handle workflow run failure', async () => { + const { runWorkflowApi } = useRunWorkflow({ router }); + + rootStore.pushConnectionActive = true; + vi.mocked(workflowsStore).runWorkflow.mockRejectedValue(new Error('Failed to run workflow')); + + await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow('Failed to run workflow'); + expect(uiStore.removeActiveAction).toHaveBeenCalledWith('workflowRunning'); + }); + + it('should set waitingForWebhook if response indicates waiting', async () => { + const { runWorkflowApi } = useRunWorkflow({ router }); + + rootStore.pushConnectionActive = true; + const mockResponse = { executionId: '123', waitingForWebhook: true }; + vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse); + + const response = await runWorkflowApi({} as IStartRunData); + + expect(response).toEqual(mockResponse); + expect(workflowsStore.executionWaitingForWebhook).toBe(true); + }); + }); + + describe('runWorkflow()', () => { + it('should return undefined if UI action "workflowRunning" is active', async () => { + const { runWorkflow } = useRunWorkflow({ router }); + vi.mocked(uiStore).isActionActive.mockReturnValue(true); + const result = await runWorkflow({}); + expect(result).toBeUndefined(); + }); + + it('should handle workflow issues correctly', async () => { + const { runWorkflow } = useRunWorkflow({ router }); + + vi.mocked(uiStore).isActionActive.mockReturnValue(false); + vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ + name: 'Test Workflow', + } as unknown as Workflow); + vi.mocked(workflowsStore).nodesIssuesExist = true; + vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {}); + vi.mocked(workflowHelpers).checkReadyForExecution.mockReturnValue({ + someNode: { issues: { input: ['issue'] } }, + }); + + const result = await runWorkflow({}); + expect(result).toBeUndefined(); + expect(toast.showMessage).toHaveBeenCalled(); + }); + + it('should execute workflow successfully', async () => { + const mockExecutionResponse = { executionId: '123' }; + const { runWorkflow } = useRunWorkflow({ router }); + + vi.mocked(rootStore).pushConnectionActive = true; + vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); + vi.mocked(workflowsStore).nodesIssuesExist = false; + vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ + name: 'Test Workflow', + } as Workflow); + vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {}); + vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ + id: 'workflowId', + nodes: [], + } as unknown as IWorkflowData); + vi.mocked(workflowsStore).getWorkflowRunData = { + NodeName: [], + }; + + const result = await runWorkflow({}); + expect(result).toEqual(mockExecutionResponse); + }); + }); + + describe('consolidateRunDataAndStartNodes()', () => { + it('should return empty runData and startNodeNames if runData is null', () => { + const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router }); + const workflowMock = { + getParentNodes: vi.fn(), + nodes: {}, + } as unknown as Workflow; + + const result = consolidateRunDataAndStartNodes([], null, undefined, workflowMock); + expect(result).toEqual({ runData: undefined, startNodeNames: [] }); + }); + + it('should return correct startNodeNames and newRunData for given directParentNodes and runData', () => { + const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router }); + const directParentNodes = ['node1', 'node2']; + const runData = { + node2: [{ data: { main: [[{ json: { value: 'data2' } }]] } }], + node3: [{ data: { main: [[{ json: { value: 'data3' } }]] } }], + } as unknown as IRunData; + const pinData: IPinData = { + node2: [{ json: { value: 'data2' } }], + }; + const workflowMock = { + getParentNodes: vi.fn().mockImplementation((node) => { + if (node === 'node1') return ['node3']; + return []; + }), + nodes: { + node1: { disabled: false }, + node2: { disabled: false }, + node3: { disabled: true }, + }, + } as unknown as Workflow; + + const result = consolidateRunDataAndStartNodes( + directParentNodes, + runData, + pinData, + workflowMock, + ); + + expect(result.startNodeNames).toContain('node1'); + expect(result.startNodeNames).not.toContain('node3'); + expect(result.runData).toEqual(runData); + }); + + it('should include directParentNode in startNodeNames if it has no runData or pinData', () => { + const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router }); + const directParentNodes = ['node1']; + const runData = { + node2: [ + { + data: { + main: [[{ json: { value: 'data2' } }]], + }, + }, + ], + } as unknown as IRunData; + const workflowMock = { + getParentNodes: vi.fn().mockReturnValue([]), + nodes: { node1: { disabled: false } }, + } as unknown as Workflow; + + const result = consolidateRunDataAndStartNodes( + directParentNodes, + runData, + undefined, + workflowMock, + ); + + expect(result.startNodeNames).toContain('node1'); + expect(result.runData).toBeUndefined(); + }); + }); +}); diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts new file mode 100644 index 0000000000..764bef62db --- /dev/null +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -0,0 +1,397 @@ +import type { + IExecutionPushResponse, + IExecutionResponse, + IStartRunData, + IWorkflowDb, +} from '@/Interface'; +import type { + IDataObject, + IRunData, + IRunExecutionData, + ITaskData, + IPinData, + IWorkflowBase, + Workflow, + StartNodeData, +} from 'n8n-workflow'; +import { + NodeHelpers, + NodeConnectionType, + TelemetryHelpers, + FORM_TRIGGER_PATH_IDENTIFIER, +} from 'n8n-workflow'; + +import { useToast } from '@/composables/useToast'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; + +import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants'; +import { useTitleChange } from '@/composables/useTitleChange'; +import { useRootStore } from '@/stores/n8nRoot.store'; +import { useUIStore } from '@/stores/ui.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { openPopUpWindow } from '@/utils/executionUtils'; +import { useExternalHooks } from '@/composables/useExternalHooks'; +import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import type { useRouter } from 'vue-router'; +import { isEmpty } from '@/utils/typesUtils'; +import { useI18n } from '@/composables/useI18n'; +import { useTelemetry } from '@/composables/useTelemetry'; +import { get } from 'lodash-es'; + +export function useRunWorkflow(options: { router: ReturnType }) { + const nodeHelpers = useNodeHelpers(); + const workflowHelpers = useWorkflowHelpers({ router: options.router }); + const i18n = useI18n(); + const telemetry = useTelemetry(); + const toast = useToast(); + const { titleSet } = useTitleChange(); + + const rootStore = useRootStore(); + const uiStore = useUIStore(); + const workflowsStore = useWorkflowsStore(); + + // Starts to execute a workflow on server + async function runWorkflowApi(runData: IStartRunData): Promise { + if (!rootStore.pushConnectionActive) { + // Do not start if the connection to server is not active + // because then it can not receive the data as it executes. + throw new Error(i18n.baseText('workflowRun.noActiveConnectionToTheServer')); + } + + workflowsStore.subWorkflowExecutionError = null; + + uiStore.addActiveAction('workflowRunning'); + + let response: IExecutionPushResponse; + + try { + response = await workflowsStore.runWorkflow(runData); + } catch (error) { + uiStore.removeActiveAction('workflowRunning'); + throw error; + } + + if (response.executionId !== undefined) { + workflowsStore.activeExecutionId = response.executionId; + } + + if (response.waitingForWebhook === true) { + workflowsStore.executionWaitingForWebhook = true; + } + + return response; + } + + async function runWorkflow(options: { + destinationNode?: string; + triggerNode?: string; + nodeData?: ITaskData; + source?: string; + }): Promise { + const workflow = workflowHelpers.getCurrentWorkflow(); + + if (uiStore.isActionActive('workflowRunning')) { + return; + } + + titleSet(workflow.name as string, 'EXECUTING'); + + toast.clearAllStickyNotifications(); + + try { + // Check first if the workflow has any issues before execute it + nodeHelpers.refreshNodeIssues(); + const issuesExist = workflowsStore.nodesIssuesExist; + if (issuesExist) { + // If issues exist get all of the issues of all nodes + const workflowIssues = workflowHelpers.checkReadyForExecution( + workflow, + options.destinationNode, + ); + if (workflowIssues !== null) { + const errorMessages = []; + let nodeIssues: string[]; + const trackNodeIssues: Array<{ + node_type: string; + error: string; + }> = []; + const trackErrorNodeTypes: string[] = []; + for (const nodeName of Object.keys(workflowIssues)) { + nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]); + let issueNodeType = 'UNKNOWN'; + const issueNode = workflowsStore.getNodeByName(nodeName); + + if (issueNode) { + issueNodeType = issueNode.type; + } + + trackErrorNodeTypes.push(issueNodeType); + const trackNodeIssue = { + node_type: issueNodeType, + error: '', + caused_by_credential: !!workflowIssues[nodeName].credentials, + }; + + for (const nodeIssue of nodeIssues) { + errorMessages.push( + `${nodeName}: ${nodeIssue}`, + ); + trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue); + } + trackNodeIssues.push(trackNodeIssue); + } + + toast.showMessage({ + title: i18n.baseText('workflowRun.showMessage.title'), + message: errorMessages.join('
'), + type: 'error', + duration: 0, + }); + titleSet(workflow.name as string, 'ERROR'); + void useExternalHooks().run('workflowRun.runError', { + errorMessages, + nodeName: options.destinationNode, + }); + + await workflowHelpers.getWorkflowDataToSave().then((workflowData) => { + telemetry.track('Workflow execution preflight failed', { + workflow_id: workflow.id, + workflow_name: workflow.name, + execution_type: options.destinationNode || options.triggerNode ? 'node' : 'workflow', + node_graph_string: JSON.stringify( + TelemetryHelpers.generateNodesGraph( + workflowData as IWorkflowBase, + workflowHelpers.getNodeTypes(), + ).nodeGraph, + ), + error_node_types: JSON.stringify(trackErrorNodeTypes), + errors: JSON.stringify(trackNodeIssues), + }); + }); + return; + } + } + + // Get the direct parents of the node + let directParentNodes: string[] = []; + if (options.destinationNode !== undefined) { + directParentNodes = workflow.getParentNodes( + options.destinationNode, + NodeConnectionType.Main, + 1, + ); + } + + const runData = workflowsStore.getWorkflowRunData; + + if (workflowsStore.isNewWorkflow) { + await workflowHelpers.saveCurrentWorkflow(); + } + + const workflowData = await workflowHelpers.getWorkflowDataToSave(); + + const consolidatedData = consolidateRunDataAndStartNodes( + directParentNodes, + runData, + workflowData.pinData, + workflow, + ); + + const { startNodeNames } = consolidatedData; + let { runData: newRunData } = consolidatedData; + let executedNode: string | undefined; + if ( + startNodeNames.length === 0 && + 'destinationNode' in options && + options.destinationNode !== undefined + ) { + executedNode = options.destinationNode; + startNodeNames.push(options.destinationNode); + } else if ('triggerNode' in options && 'nodeData' in options) { + startNodeNames.push( + ...workflow.getChildNodes(options.triggerNode as string, NodeConnectionType.Main, 1), + ); + newRunData = { + [options.triggerNode as string]: [options.nodeData], + } as IRunData; + executedNode = options.triggerNode; + } + + const startNodes: StartNodeData[] = startNodeNames.map((name) => { + // Find for each start node the source data + let sourceData = get(runData, [name, 0, 'source', 0], null); + if (sourceData === null) { + const parentNodes = workflow.getParentNodes(name, NodeConnectionType.Main, 1); + const executeData = workflowHelpers.executeData( + parentNodes, + name, + NodeConnectionType.Main, + 0, + ); + sourceData = get(executeData, ['source', NodeConnectionType.Main, 0], null); + } + return { + name, + sourceData, + }; + }); + + const startRunData: IStartRunData = { + workflowData, + runData: newRunData, + pinData: workflowData.pinData, + startNodes, + }; + if ('destinationNode' in options) { + startRunData.destinationNode = options.destinationNode; + } + + // Init the execution data to represent the start of the execution + // that data which gets reused is already set and data of newly executed + // nodes can be added as it gets pushed in + const executionData: IExecutionResponse = { + id: '__IN_PROGRESS__', + finished: false, + mode: 'manual', + status: 'running', + startedAt: new Date(), + stoppedAt: undefined, + workflowId: workflow.id, + executedNode, + data: { + resultData: { + runData: newRunData || {}, + pinData: workflowData.pinData, + workflowData, + }, + } as IRunExecutionData, + workflowData: { + id: workflowsStore.workflowId, + name: workflowData.name!, + active: workflowData.active!, + createdAt: 0, + updatedAt: 0, + ...workflowData, + } as IWorkflowDb, + }; + workflowsStore.setWorkflowExecutionData(executionData); + nodeHelpers.updateNodesExecutionIssues(); + + const runWorkflowApiResponse = await runWorkflowApi(startRunData); + + for (const node of workflowData.nodes) { + if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) { + continue; + } + + if ( + options.destinationNode && + options.destinationNode !== node.name && + !directParentNodes.includes(node.name) + ) { + continue; + } + + if (node.name === options.destinationNode || !node.disabled) { + let testUrl = ''; + + if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion === 1) { + const webhookPath = (node.parameters.path as string) || node.webhookId; + testUrl = `${rootStore.getWebhookTestUrl}/${webhookPath}/${FORM_TRIGGER_PATH_IDENTIFIER}`; + } + + if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion > 1) { + const webhookPath = (node.parameters.path as string) || node.webhookId; + testUrl = `${rootStore.getFormTestUrl}/${webhookPath}`; + } + + if ( + node.type === WAIT_NODE_TYPE && + node.parameters.resume === 'form' && + runWorkflowApiResponse.executionId + ) { + const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name); + + const showForm = + options.destinationNode === node.name || + directParentNodes.includes(node.name) || + workflowTriggerNodes.some((triggerNode) => + workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name), + ); + + if (!showForm) continue; + + const { webhookSuffix } = (node.parameters.options || {}) as IDataObject; + const suffix = webhookSuffix ? `/${webhookSuffix}` : ''; + testUrl = `${rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`; + } + + if (testUrl) openPopUpWindow(testUrl); + } + } + + await useExternalHooks().run('workflowRun.runWorkflow', { + nodeName: options.destinationNode, + source: options.source, + }); + + return runWorkflowApiResponse; + } catch (error) { + titleSet(workflow.name as string, 'ERROR'); + toast.showError(error, i18n.baseText('workflowRun.showError.title')); + return undefined; + } + } + + function consolidateRunDataAndStartNodes( + directParentNodes: string[], + runData: IRunData | null, + pinData: IPinData | undefined, + workflow: Workflow, + ): { runData: IRunData | undefined; startNodeNames: string[] } { + const startNodeNames: string[] = []; + let newRunData: IRunData | undefined; + + if (runData !== null && Object.keys(runData).length !== 0) { + newRunData = {}; + // Go over the direct parents of the node + for (const directParentNode of directParentNodes) { + // Go over the parents of that node so that we can get a start + // node for each of the branches + const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main); + + // Add also the enabled direct parent to be checked + if (workflow.nodes[directParentNode].disabled) continue; + + parentNodes.push(directParentNode); + + for (const parentNode of parentNodes) { + if (!runData[parentNode]?.length && !pinData?.[parentNode]?.length) { + // When we hit a node which has no data we stop and set it + // as a start node the execution from and then go on with other + // direct input nodes + startNodeNames.push(parentNode); + break; + } + if (runData[parentNode]) { + newRunData[parentNode] = runData[parentNode]?.slice(0, 1); + } + } + } + + if (isEmpty(newRunData)) { + // If there is no data for any of the parent nodes make sure + // that run data is empty that it runs regularly + newRunData = undefined; + } + } + + return { runData: newRunData, startNodeNames }; + } + + return { + consolidateRunDataAndStartNodes, + runWorkflow, + runWorkflowApi, + }; +} diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index f32d1bf6a5..62a77ecd72 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -30,12 +30,17 @@ export function useToast() { const externalHooks = useExternalHooks(); const i18n = useI18n(); - function showMessage(messageData: NotificationOptions, track = true) { + function showMessage(messageData: Partial, track = true) { messageData = { ...messageDefaults, ...messageData }; - messageData.message = - typeof messageData.message === 'string' - ? sanitizeHtml(messageData.message) - : messageData.message; + + Object.defineProperty(messageData, 'message', { + value: + typeof messageData.message === 'string' + ? sanitizeHtml(messageData.message) + : messageData.message, + writable: true, + enumerable: true, + }); const notification = Notification(messageData); @@ -116,7 +121,7 @@ export function useToast() { } function showError(e: Error | unknown, title: string, message?: string) { - const error = e as Error; + const error = e as NotificationErrorWithNodeAndDescription; const messageLine = message ? `${message}
` : ''; showMessage( { @@ -124,7 +129,7 @@ export function useToast() { message: ` ${messageLine} ${error.message} - ${collapsableDetails(error as NotificationErrorWithNodeAndDescription)}`, + ${collapsableDetails(error)}`, type: 'error', duration: 0, }, @@ -170,7 +175,7 @@ export function useToast() { function showNotificationForViews(views: VIEWS[]) { const notifications: NotificationOptions[] = []; views.forEach((view) => { - notifications.push(...uiStore.getNotificationsForView(view)); + notifications.push(...(uiStore.getNotificationsForView(view) as NotificationOptions[])); }); if (notifications.length) { notifications.forEach(async (notification) => { diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index 27e24e336b..79bd573c67 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -71,7 +71,7 @@ import { useCanvasStore } from '@/stores/canvas.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { tryToParseNumber } from '@/utils/typesUtils'; import { useI18n } from '@/composables/useI18n'; -import type { Router } from 'vue-router'; +import type { useRouter } from 'vue-router'; import { useTelemetry } from '@/composables/useTelemetry'; export function resolveParameter( @@ -451,7 +451,8 @@ export function executeData( return executeData; } -export function useWorkflowHelpers(router: Router) { +export function useWorkflowHelpers(options: { router: ReturnType }) { + const router = options.router; const nodeTypesStore = useNodeTypesStore(); const rootStore = useRootStore(); const templatesStore = useTemplatesStore(); diff --git a/packages/editor-ui/src/mixins/expressionManager.ts b/packages/editor-ui/src/mixins/expressionManager.ts index 17ec7c15e5..e6137864b8 100644 --- a/packages/editor-ui/src/mixins/expressionManager.ts +++ b/packages/editor-ui/src/mixins/expressionManager.ts @@ -204,7 +204,7 @@ export const expressionManager = defineComponent({ try { const ndvStore = useNDVStore(); - const workflowHelpers = useWorkflowHelpers(this.$router); + const workflowHelpers = useWorkflowHelpers({ router: this.$router }); if (!ndvStore.activeNode) { // e.g. credential modal result.resolved = Expression.resolveWithoutWorkflow(resolvable, this.additionalData); diff --git a/packages/editor-ui/src/mixins/pushConnection.ts b/packages/editor-ui/src/mixins/pushConnection.ts index 8d7e099cf5..8e4aa48984 100644 --- a/packages/editor-ui/src/mixins/pushConnection.ts +++ b/packages/editor-ui/src/mixins/pushConnection.ts @@ -44,7 +44,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; export const pushConnection = defineComponent({ setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); const nodeHelpers = useNodeHelpers(); return { ...useTitleChange(), diff --git a/packages/editor-ui/src/mixins/workflowActivate.ts b/packages/editor-ui/src/mixins/workflowActivate.ts index e7666e4cf9..185e8c7bee 100644 --- a/packages/editor-ui/src/mixins/workflowActivate.ts +++ b/packages/editor-ui/src/mixins/workflowActivate.ts @@ -19,7 +19,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; export const workflowActivate = defineComponent({ setup() { const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); return { workflowHelpers, ...useToast(), diff --git a/packages/editor-ui/src/mixins/workflowRun.ts b/packages/editor-ui/src/mixins/workflowRun.ts deleted file mode 100644 index c873b2d754..0000000000 --- a/packages/editor-ui/src/mixins/workflowRun.ts +++ /dev/null @@ -1,393 +0,0 @@ -import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface'; -import { mapStores } from 'pinia'; -import { defineComponent } from 'vue'; -import { get } from 'lodash-es'; - -import type { - IDataObject, - IRunData, - IRunExecutionData, - ITaskData, - IPinData, - IWorkflowBase, - Workflow, - StartNodeData, -} from 'n8n-workflow'; -import { - NodeHelpers, - NodeConnectionType, - TelemetryHelpers, - FORM_TRIGGER_PATH_IDENTIFIER, -} from 'n8n-workflow'; - -import { useToast } from '@/composables/useToast'; -import { useNodeHelpers } from '@/composables/useNodeHelpers'; - -import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants'; -import { useTitleChange } from '@/composables/useTitleChange'; -import { useRootStore } from '@/stores/n8nRoot.store'; -import { useUIStore } from '@/stores/ui.store'; -import { useWorkflowsStore } from '@/stores/workflows.store'; -import { openPopUpWindow } from '@/utils/executionUtils'; -import { useExternalHooks } from '@/composables/useExternalHooks'; -import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; -import { useRouter } from 'vue-router'; -import { isEmpty } from '@/utils/typesUtils'; - -export const consolidateRunDataAndStartNodes = ( - directParentNodes: string[], - runData: IRunData | null, - pinData: IPinData | undefined, - workflow: Workflow, -): { runData: IRunData | undefined; startNodeNames: string[] } => { - const startNodeNames: string[] = []; - let newRunData: IRunData | undefined; - - if (runData !== null && Object.keys(runData).length !== 0) { - newRunData = {}; - // Go over the direct parents of the node - for (const directParentNode of directParentNodes) { - // Go over the parents of that node so that we can get a start - // node for each of the branches - const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main); - - // Add also the enabled direct parent to be checked - if (workflow.nodes[directParentNode].disabled) continue; - - parentNodes.push(directParentNode); - - for (const parentNode of parentNodes) { - if (!runData[parentNode]?.length && !pinData?.[parentNode]?.length) { - // When we hit a node which has no data we stop and set it - // as a start node the execution from and then go on with other - // direct input nodes - startNodeNames.push(parentNode); - break; - } - if (runData[parentNode]) { - newRunData[parentNode] = runData[parentNode]?.slice(0, 1); - } - } - } - - if (isEmpty(newRunData)) { - // If there is no data for any of the parent nodes make sure - // that run data is empty that it runs regularly - newRunData = undefined; - } - } - - return { runData: newRunData, startNodeNames }; -}; - -export const workflowRun = defineComponent({ - setup() { - const nodeHelpers = useNodeHelpers(); - const router = useRouter(); - const workflowHelpers = useWorkflowHelpers(router); - - return { - ...useTitleChange(), - ...useToast(), - nodeHelpers, - workflowHelpers, - }; - }, - computed: { - ...mapStores(useRootStore, useUIStore, useWorkflowsStore), - }, - methods: { - // Starts to executes a workflow on server. - async runWorkflowApi(runData: IStartRunData): Promise { - if (!this.rootStore.pushConnectionActive) { - // Do not start if the connection to server is not active - // because then it can not receive the data as it executes. - throw new Error(this.$locale.baseText('workflowRun.noActiveConnectionToTheServer')); - } - - this.workflowsStore.subWorkflowExecutionError = null; - - this.uiStore.addActiveAction('workflowRunning'); - - let response: IExecutionPushResponse; - - try { - response = await this.workflowsStore.runWorkflow(runData); - } catch (error) { - this.uiStore.removeActiveAction('workflowRunning'); - throw error; - } - - if (response.executionId !== undefined) { - this.workflowsStore.activeExecutionId = response.executionId; - } - - if (response.waitingForWebhook === true) { - this.workflowsStore.executionWaitingForWebhook = true; - } - - return response; - }, - - async runWorkflow( - options: - | { destinationNode: string; source?: string } - | { triggerNode: string; nodeData: ITaskData; source?: string } - | { source?: string }, - ): Promise { - const workflow = this.workflowHelpers.getCurrentWorkflow(); - - if (this.uiStore.isActionActive('workflowRunning')) { - return; - } - - this.titleSet(workflow.name as string, 'EXECUTING'); - - this.clearAllStickyNotifications(); - - try { - // Check first if the workflow has any issues before execute it - this.nodeHelpers.refreshNodeIssues(); - const issuesExist = this.workflowsStore.nodesIssuesExist; - if (issuesExist) { - // If issues exist get all of the issues of all nodes - const workflowIssues = this.workflowHelpers.checkReadyForExecution( - workflow, - options.destinationNode, - ); - if (workflowIssues !== null) { - const errorMessages = []; - let nodeIssues: string[]; - const trackNodeIssues: Array<{ - node_type: string; - error: string; - }> = []; - const trackErrorNodeTypes: string[] = []; - for (const nodeName of Object.keys(workflowIssues)) { - nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]); - let issueNodeType = 'UNKNOWN'; - const issueNode = this.workflowsStore.getNodeByName(nodeName); - - if (issueNode) { - issueNodeType = issueNode.type; - } - - trackErrorNodeTypes.push(issueNodeType); - const trackNodeIssue = { - node_type: issueNodeType, - error: '', - caused_by_credential: !!workflowIssues[nodeName].credentials, - }; - - for (const nodeIssue of nodeIssues) { - errorMessages.push( - `${nodeName}: ${nodeIssue}`, - ); - trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue); - } - trackNodeIssues.push(trackNodeIssue); - } - - this.showMessage({ - title: this.$locale.baseText('workflowRun.showMessage.title'), - message: errorMessages.join('
'), - type: 'error', - duration: 0, - }); - this.titleSet(workflow.name as string, 'ERROR'); - void useExternalHooks().run('workflowRun.runError', { - errorMessages, - nodeName: options.destinationNode, - }); - - await this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => { - this.$telemetry.track('Workflow execution preflight failed', { - workflow_id: workflow.id, - workflow_name: workflow.name, - execution_type: - options.destinationNode || options.triggerNode ? 'node' : 'workflow', - node_graph_string: JSON.stringify( - TelemetryHelpers.generateNodesGraph( - workflowData as IWorkflowBase, - this.workflowHelpers.getNodeTypes(), - ).nodeGraph, - ), - error_node_types: JSON.stringify(trackErrorNodeTypes), - errors: JSON.stringify(trackNodeIssues), - }); - }); - return; - } - } - - // Get the direct parents of the node - let directParentNodes: string[] = []; - if (options.destinationNode !== undefined) { - directParentNodes = workflow.getParentNodes( - options.destinationNode, - NodeConnectionType.Main, - 1, - ); - } - - const runData = this.workflowsStore.getWorkflowRunData; - - if (this.workflowsStore.isNewWorkflow) { - await this.workflowHelpers.saveCurrentWorkflow(); - } - - const workflowData = await this.workflowHelpers.getWorkflowDataToSave(); - - const consolidatedData = consolidateRunDataAndStartNodes( - directParentNodes, - runData, - workflowData.pinData, - workflow, - ); - - const { startNodeNames } = consolidatedData; - let { runData: newRunData } = consolidatedData; - let executedNode: string | undefined; - if ( - startNodeNames.length === 0 && - 'destinationNode' in options && - options.destinationNode !== undefined - ) { - executedNode = options.destinationNode; - startNodeNames.push(options.destinationNode); - } else if ('triggerNode' in options && 'nodeData' in options) { - startNodeNames.push( - ...workflow.getChildNodes(options.triggerNode, NodeConnectionType.Main, 1), - ); - newRunData = { - [options.triggerNode]: [options.nodeData], - }; - executedNode = options.triggerNode; - } - - const startNodes: StartNodeData[] = startNodeNames.map((name) => { - // Find for each start node the source data - let sourceData = get(runData, [name, 0, 'source', 0], null); - if (sourceData === null) { - const parentNodes = workflow.getParentNodes(name, NodeConnectionType.Main, 1); - const executeData = this.workflowHelpers.executeData( - parentNodes, - name, - NodeConnectionType.Main, - 0, - ); - sourceData = get(executeData, ['source', NodeConnectionType.Main, 0], null); - } - return { - name, - sourceData, - }; - }); - - const startRunData: IStartRunData = { - workflowData, - runData: newRunData, - pinData: workflowData.pinData, - startNodes, - }; - if ('destinationNode' in options) { - startRunData.destinationNode = options.destinationNode; - } - - // Init the execution data to represent the start of the execution - // that data which gets reused is already set and data of newly executed - // nodes can be added as it gets pushed in - const executionData: IExecutionResponse = { - id: '__IN_PROGRESS__', - finished: false, - mode: 'manual', - startedAt: new Date(), - stoppedAt: undefined, - workflowId: workflow.id, - executedNode, - data: { - resultData: { - runData: newRunData || {}, - pinData: workflowData.pinData, - workflowData, - }, - } as IRunExecutionData, - workflowData: { - id: this.workflowsStore.workflowId, - name: workflowData.name!, - active: workflowData.active!, - createdAt: 0, - updatedAt: 0, - ...workflowData, - }, - }; - this.workflowsStore.setWorkflowExecutionData(executionData); - this.nodeHelpers.updateNodesExecutionIssues(); - - const runWorkflowApiResponse = await this.runWorkflowApi(startRunData); - - for (const node of workflowData.nodes) { - if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) { - continue; - } - - if ( - options.destinationNode && - options.destinationNode !== node.name && - !directParentNodes.includes(node.name) - ) { - continue; - } - - if (node.name === options.destinationNode || !node.disabled) { - let testUrl = ''; - - if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion === 1) { - const webhookPath = (node.parameters.path as string) || node.webhookId; - testUrl = `${this.rootStore.getWebhookTestUrl}/${webhookPath}/${FORM_TRIGGER_PATH_IDENTIFIER}`; - } - - if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion > 1) { - const webhookPath = (node.parameters.path as string) || node.webhookId; - testUrl = `${this.rootStore.getFormTestUrl}/${webhookPath}`; - } - - if ( - node.type === WAIT_NODE_TYPE && - node.parameters.resume === 'form' && - runWorkflowApiResponse.executionId - ) { - const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name); - - const showForm = - options.destinationNode === node.name || - directParentNodes.includes(node.name) || - workflowTriggerNodes.some((triggerNode) => - this.workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name), - ); - - if (!showForm) continue; - - const { webhookSuffix } = (node.parameters.options || {}) as IDataObject; - const suffix = webhookSuffix ? `/${webhookSuffix}` : ''; - testUrl = `${this.rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`; - } - - if (testUrl) openPopUpWindow(testUrl); - } - } - - await useExternalHooks().run('workflowRun.runWorkflow', { - nodeName: options.destinationNode, - source: options.source, - }); - - return runWorkflowApiResponse; - } catch (error) { - this.titleSet(workflow.name as string, 'ERROR'); - this.showError(error, this.$locale.baseText('workflowRun.showError.title')); - return undefined; - } - }, - }, -}); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index efb929b22d..74e73b4176 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -259,7 +259,6 @@ import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; import { useI18n } from '@/composables/useI18n'; import { useMessage } from '@/composables/useMessage'; import { useToast } from '@/composables/useToast'; -import { workflowRun } from '@/mixins/workflowRun'; import NodeDetailsView from '@/components/NodeDetailsView.vue'; import ContextMenu from '@/components/ContextMenu/ContextMenu.vue'; @@ -382,6 +381,7 @@ import { useDebounce } from '@/composables/useDebounce'; import { useCanvasPanning } from '@/composables/useCanvasPanning'; import { tryToParseNumber } from '@/utils/typesUtils'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import { useRunWorkflow } from '@/composables/useRunWorkflow'; interface AddNodeOptions { position?: XYPosition; @@ -413,7 +413,6 @@ export default defineComponent({ ContextMenu, SetupWorkflowCredentialsButton, }, - mixins: [workflowRun], async beforeRouteLeave(to, from, next) { if ( getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS || @@ -474,7 +473,7 @@ export default defineComponent({ next(); } }, - setup(props, ctx) { + setup() { const nodeViewRootRef = ref(null); const nodeViewRef = ref(null); const onMouseMoveEnd = ref(null); @@ -492,7 +491,8 @@ export default defineComponent({ const deviceSupport = useDeviceSupport(); const { callDebounced } = useDebounce(); const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd }); - const workflowHelpers = useWorkflowHelpers(router); + const workflowHelpers = useWorkflowHelpers({ router }); + const { runWorkflow } = useRunWorkflow({ router }); return { locale, @@ -508,6 +508,7 @@ export default defineComponent({ nodeViewRef, onMouseMoveEnd, workflowHelpers, + runWorkflow, callDebounced, ...useCanvasMouseSelect(), ...useGlobalLinkActions(), @@ -516,8 +517,6 @@ export default defineComponent({ ...useMessage(), ...useUniqueNodeName(), ...useExecutionDebugging(), - // eslint-disable-next-line @typescript-eslint/no-misused-promises - ...workflowRun.setup?.(props, ctx), }; }, watch: { @@ -1907,7 +1906,7 @@ export default defineComponent({ const nodeData = JSON.stringify(workflowToCopy, null, 2); - this.clipboard.copy(nodeData); + void this.clipboard.copy(nodeData); if (data.nodes.length > 0) { if (!isCut) { this.showMessage({