diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 73e2a897f6..1855bdb43b 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -592,4 +592,31 @@ describe('Execution', () => { cy.wait(100); workflowPage.getters.errorToast({ timeout: 1 }).should('not.exist'); }); + + it('should execute workflow partially up to the node that has issues', () => { + cy.createFixtureWorkflow( + 'Test_workflow_partial_execution_with_missing_credentials.json', + 'My test workflow', + ); + + cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + + workflowPage.getters.zoomToFitButton().click(); + workflowPage.getters.executeWorkflowButton().click(); + + // Wait for the execution to return. + cy.wait('@workflowRun'); + + // Check that the previous nodes executed successfully + workflowPage.getters + .canvasNodeByName('DebugHelper') + .within(() => cy.get('.fa-check')) + .should('exist'); + workflowPage.getters + .canvasNodeByName('Filter') + .within(() => cy.get('.fa-check')) + .should('exist'); + + workflowPage.getters.errorToast().should('contain', `Problem in node ‘Telegram‘`); + }); }); diff --git a/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json new file mode 100644 index 0000000000..2a9e75e11b --- /dev/null +++ b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json @@ -0,0 +1,115 @@ +{ + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "2be09fdcb9594c0827fd4cee80f7e590c93297d9217685f34c2250fe3144ef0c" + }, + "nodes": [ + { + "parameters": {}, + "id": "09e4325e-ede1-40cf-a1ba-58612bbc7f1b", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 400 + ] + }, + { + "parameters": { + "category": "randomData" + }, + "id": "4920bf3a-9978-4196-9dcb-8c2892e5641b", + "name": "DebugHelper", + "type": "n8n-nodes-base.debugHelper", + "typeVersion": 1, + "position": [ + 1040, + 400 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "7508343e-3e99-4d12-96e4-00a35a3d4306", + "leftValue": "={{ $json.email }}", + "rightValue": ".", + "operator": { + "type": "string", + "operation": "contains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "4f6a6a4e-19b6-43f5-ba5c-e40b09d7f873", + "name": "Filter", + "type": "n8n-nodes-base.filter", + "typeVersion": 2, + "position": [ + 1260, + 400 + ] + }, + { + "parameters": { + "chatId": "123123", + "text": "1123123", + "additionalFields": {} + }, + "id": "1765f352-fc12-4fab-9c24-d666a150266f", + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.1, + "position": [ + 1480, + 400 + ] + } + ], + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ] + ] + }, + "DebugHelper": { + "main": [ + [ + { + "node": "Filter", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter": { + "main": [ + [ + { + "node": "Telegram", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/editor-ui/src/composables/useRunWorkflow.test.ts index 2908e50820..04e83ef123 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.test.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.test.ts @@ -5,9 +5,7 @@ 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'; @@ -70,7 +68,6 @@ vi.mock('@/composables/useWorkflowHelpers', () => ({ vi.mock('@/composables/useNodeHelpers', () => ({ useNodeHelpers: vi.fn().mockReturnValue({ - refreshNodeIssues: vi.fn(), updateNodesExecutionIssues: vi.fn(), }), })); @@ -94,9 +91,7 @@ describe('useRunWorkflow({ router })', () => { let uiStore: ReturnType; let workflowsStore: ReturnType; let router: ReturnType; - let toast: ReturnType; let workflowHelpers: ReturnType; - let nodeHelpers: ReturnType; beforeAll(() => { const pinia = createTestingPinia(); @@ -108,9 +103,7 @@ describe('useRunWorkflow({ router })', () => { workflowsStore = useWorkflowsStore(); router = useRouter(); - toast = useToast(); workflowHelpers = useWorkflowHelpers({ router }); - nodeHelpers = useNodeHelpers(); }); describe('runWorkflowApi()', () => { @@ -170,22 +163,26 @@ describe('useRunWorkflow({ router })', () => { expect(result).toBeUndefined(); }); - it('should handle workflow issues correctly', async () => { + it('should execute workflow even if it has issues', async () => { + const mockExecutionResponse = { executionId: '123' }; 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).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).nodesIssuesExist = true; - vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {}); - vi.mocked(workflowHelpers).checkReadyForExecution.mockReturnValue({ - someNode: { issues: { input: ['issue'] } }, - }); + vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ + id: 'workflowId', + nodes: [], + } as unknown as IWorkflowData); + vi.mocked(workflowsStore).getWorkflowRunData = { + NodeName: [], + }; const result = await runWorkflow({}); - expect(result).toBeUndefined(); - expect(toast.showMessage).toHaveBeenCalled(); + expect(result).toEqual(mockExecutionResponse); }); it('should execute workflow successfully', async () => { @@ -198,7 +195,6 @@ describe('useRunWorkflow({ router })', () => { vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ name: 'Test Workflow', } as Workflow); - vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {}); vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ id: 'workflowId', nodes: [], diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts index 4de294d02d..4e2b73e317 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -11,17 +11,11 @@ import type { IRunExecutionData, ITaskData, IPinData, - IWorkflowBase, Workflow, StartNodeData, IRun, } from 'n8n-workflow'; -import { - NodeHelpers, - NodeConnectionType, - TelemetryHelpers, - FORM_TRIGGER_PATH_IDENTIFIER, -} from 'n8n-workflow'; +import { NodeConnectionType, FORM_TRIGGER_PATH_IDENTIFIER } from 'n8n-workflow'; import { useToast } from '@/composables/useToast'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; @@ -42,14 +36,12 @@ 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 }) { +export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType }) { const nodeHelpers = useNodeHelpers(); - const workflowHelpers = useWorkflowHelpers({ router: options.router }); + const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router }); const i18n = useI18n(); - const telemetry = useTelemetry(); const toast = useToast(); const { titleSet } = useTitleChange(); @@ -106,79 +98,6 @@ export function useRunWorkflow(options: { router: ReturnType } 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) { @@ -319,7 +238,7 @@ export function useRunWorkflow(options: { router: ReturnType } executedNode, data: { resultData: { - runData: newRunData || {}, + runData: newRunData ?? {}, pinData: workflowData.pinData, workflowData, }, @@ -372,7 +291,9 @@ export function useRunWorkflow(options: { router: ReturnType } node.parameters.resume === 'form' && runWorkflowApiResponse.executionId ) { - const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name); + const workflowTriggerNodes = workflow + .getTriggerNodes() + .map((triggerNode) => triggerNode.name); const showForm = options.destinationNode === node.name || @@ -383,7 +304,7 @@ export function useRunWorkflow(options: { router: ReturnType } if (!showForm) continue; - const { webhookSuffix } = (node.parameters.options || {}) as IDataObject; + const { webhookSuffix } = (node.parameters.options ?? {}) as IDataObject; const suffix = webhookSuffix ? `/${webhookSuffix}` : ''; testUrl = `${rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`; } diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index 42cfa76476..2b99fee6f2 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -515,7 +515,7 @@ export function useWorkflowHelpers(options: { router: ReturnType