diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index c5fde9df5d..89647eee6c 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -658,4 +658,18 @@ describe('NDV', () => { cy.realPress('Escape'); }); }); + + it('Stop listening for trigger event from NDV', () => { + workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', { + keepNdvOpen: true, + action: 'On Changes To A Specific File', + isTrigger: true, + }); + ndv.getters.triggerPanelExecuteButton().should('exist'); + ndv.getters.triggerPanelExecuteButton().click(); + ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening'); + ndv.getters.triggerPanelExecuteButton().click(); + ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step'); + workflowPage.getters.successToast().should('exist'); + }); }); diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 5014cdbc09..cce79d4b5e 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -145,19 +145,20 @@ export class WorkflowPage extends BasePage { }, addInitialNodeToCanvas: ( nodeDisplayName: string, - opts?: { keepNdvOpen?: boolean; action?: string }, + opts?: { keepNdvOpen?: boolean; action?: string, isTrigger?: boolean}, ) => { this.getters.canvasPlusButton().click(); this.getters.nodeCreatorSearchBar().type(nodeDisplayName); this.getters.nodeCreatorSearchBar().type('{enter}'); if (opts?.action) { + const itemId = opts.isTrigger ? 'Triggers' : 'Actions'; // Expand actions category if it's collapsed nodeCreator.getters - .getCategoryItem('Actions') + .getCategoryItem(itemId) .parent() .then(($el) => { if ($el.attr('data-category-collapsed') === 'true') { - nodeCreator.getters.getCategoryItem('Actions').click(); + nodeCreator.getters.getCategoryItem(itemId).click(); } }); nodeCreator.getters.getCreatorItem(opts.action).click(); diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index 9aa11ee7bc..480a16a1db 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -77,18 +77,20 @@ export default defineComponent({ type: Boolean, }, }, + emits: ['stopExecution', 'execute'], 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 }); + const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router }); return { externalHooks, pinnedData, runWorkflow, + stopCurrentExecution, ...useToast(), ...useMessage(), }; @@ -236,6 +238,7 @@ export default defineComponent({ } else if (this.isListeningForEvents) { await this.stopWaitingForWebhook(); } else if (this.isListeningForWorkflowEvents) { + await this.stopCurrentExecution(); this.$emit('stopExecution'); } else { let shouldUnpinAndExecute = false; diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts index 6d642b3436..4de294d02d 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -1,6 +1,7 @@ import type { IExecutionPushResponse, IExecutionResponse, + IPushDataExecutionFinished, IStartRunData, IWorkflowDb, } from '@/Interface'; @@ -13,6 +14,7 @@ import type { IWorkflowBase, Workflow, StartNodeData, + IRun, } from 'n8n-workflow'; import { NodeHelpers, @@ -449,9 +451,67 @@ export function useRunWorkflow(options: { router: ReturnType } return { runData: newRunData, startNodeNames }; } + async function stopCurrentExecution() { + const executionId = workflowsStore.activeExecutionId; + if (executionId === null) { + return; + } + + try { + await workflowsStore.stopCurrentExecution(executionId); + } catch (error) { + // Execution stop might fail when the execution has already finished. Let's treat this here. + const execution = await this.workflowsStore.getExecution(executionId); + + if (execution === undefined) { + // execution finished but was not saved (e.g. due to low connectivity) + workflowsStore.finishActiveExecution({ + executionId, + data: { finished: true, stoppedAt: new Date() }, + }); + workflowsStore.executingNode.length = 0; + uiStore.removeActiveAction('workflowRunning'); + + titleSet(workflowsStore.workflowName, 'IDLE'); + toast.showMessage({ + title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'), + message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.message'), + type: 'success', + }); + } else if (execution?.finished) { + // execution finished before it could be stopped + const executedData = { + data: execution.data, + finished: execution.finished, + mode: execution.mode, + startedAt: execution.startedAt, + stoppedAt: execution.stoppedAt, + } as IRun; + const pushData = { + data: executedData, + executionId, + retryOf: execution.retryOf, + } as IPushDataExecutionFinished; + workflowsStore.finishActiveExecution(pushData); + titleSet(execution.workflowData.name, 'IDLE'); + workflowsStore.executingNode.length = 0; + workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse); + uiStore.removeActiveAction('workflowRunning'); + toast.showMessage({ + title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.title'), + message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.message'), + type: 'success', + }); + } else { + toast.showError(error, i18n.baseText('nodeView.showError.stopExecution.title')); + } + } + } + return { consolidateRunDataAndStartNodes, runWorkflow, runWorkflowApi, + stopCurrentExecution, }; } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 47863f5c19..8e789ff7d1 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -279,7 +279,6 @@ import type { INodeTypeDescription, INodeTypeNameVersion, IPinData, - IRun, ITaskData, ITelemetryTrackProperties, IWorkflowBase, @@ -303,7 +302,6 @@ import type { IUpdateInformation, IWorkflowDataUpdate, XYPosition, - IPushDataExecutionFinished, ITag, INewWorkflowData, IWorkflowTemplate, @@ -492,7 +490,7 @@ export default defineComponent({ const { callDebounced } = useDebounce(); const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd }); const workflowHelpers = useWorkflowHelpers({ router }); - const { runWorkflow } = useRunWorkflow({ router }); + const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router }); return { locale, @@ -509,6 +507,7 @@ export default defineComponent({ onMouseMoveEnd, workflowHelpers, runWorkflow, + stopCurrentExecution, callDebounced, ...useCanvasMouseSelect(), ...useGlobalLinkActions(), @@ -1930,67 +1929,8 @@ export default defineComponent({ }); }, async stopExecution() { - const executionId = this.workflowsStore.activeExecutionId; - if (executionId === null) { - return; - } - - try { - this.stopExecutionInProgress = true; - await this.workflowsStore.stopCurrentExecution(executionId); - } catch (error) { - // Execution stop might fail when the execution has already finished. Let's treat this here. - const execution = await this.workflowsStore.getExecution(executionId); - - if (execution === undefined) { - // execution finished but was not saved (e.g. due to low connectivity) - - this.workflowsStore.finishActiveExecution({ - executionId, - data: { finished: true, stoppedAt: new Date() }, - }); - this.workflowsStore.executingNode.length = 0; - this.uiStore.removeActiveAction('workflowRunning'); - - this.titleSet(this.workflowsStore.workflowName, 'IDLE'); - this.showMessage({ - title: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'), - message: this.$locale.baseText( - 'nodeView.showMessage.stopExecutionCatch.unsaved.message', - ), - type: 'success', - }); - } else if (execution?.finished) { - // execution finished before it could be stopped - - const executedData = { - data: execution.data, - finished: execution.finished, - mode: execution.mode, - startedAt: execution.startedAt, - stoppedAt: execution.stoppedAt, - } as IRun; - const pushData = { - data: executedData, - executionId, - retryOf: execution.retryOf, - } as IPushDataExecutionFinished; - this.workflowsStore.finishActiveExecution(pushData); - this.titleSet(execution.workflowData.name, 'IDLE'); - this.workflowsStore.executingNode.length = 0; - this.workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse); - this.uiStore.removeActiveAction('workflowRunning'); - this.showMessage({ - title: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.title'), - message: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.message'), - type: 'success', - }); - } else { - this.showError(error, this.$locale.baseText('nodeView.showError.stopExecution.title')); - } - } + await this.stopCurrentExecution(); this.stopExecutionInProgress = false; - void this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => { const trackProps = { workflow_id: this.workflowsStore.workflowId,