Skip to content

Commit

Permalink
feat(editor): Add ‘execute workflow’ buttons below triggers on the ca…
Browse files Browse the repository at this point in the history
…nvas (#12769)

Co-authored-by: Danny Martini <[email protected]>
Co-authored-by: Mutasem Aldmour <[email protected]>
  • Loading branch information
3 people authored Feb 10, 2025
1 parent e925562 commit b17cbec
Show file tree
Hide file tree
Showing 28 changed files with 957 additions and 168 deletions.
4 changes: 4 additions & 0 deletions cypress/composables/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

import { getVisiblePopper, getVisibleSelect } from '../utils/popper';

export function getNdvContainer() {
return cy.getByTestId('ndv');
}

export function getCredentialSelect(eq = 0) {
return cy.getByTestId('node-credentials-select').eq(eq);
}
Expand Down
8 changes: 4 additions & 4 deletions cypress/composables/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ export function getNodeCreatorItems() {
return cy.getByTestId('item-iterator-item');
}

export function getExecuteWorkflowButton() {
return cy.getByTestId('execute-workflow-button');
export function getExecuteWorkflowButton(triggerNodeName?: string) {
return cy.getByTestId(`execute-workflow-button${triggerNodeName ? `-${triggerNodeName}` : ''}`);
}

export function getManualChatButton() {
Expand Down Expand Up @@ -294,8 +294,8 @@ export function addRetrieverNodeToParent(nodeName: string, parentNodeName: strin
addSupplementalNodeToParent(nodeName, 'ai_retriever', parentNodeName);
}

export function clickExecuteWorkflowButton() {
getExecuteWorkflowButton().click();
export function clickExecuteWorkflowButton(triggerNodeName?: string) {
getExecuteWorkflowButton(triggerNodeName).click();
}

export function clickManualChatButton() {
Expand Down
41 changes: 41 additions & 0 deletions cypress/e2e/19-execution.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { clickGetBackToCanvas, getNdvContainer, getOutputTableRow } from '../composables/ndv';
import {
clickExecuteWorkflowButton,
getExecuteWorkflowButton,
getNodeByName,
getZoomToFitButton,
openNode,
} from '../composables/workflow';
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
Expand Down Expand Up @@ -214,6 +222,39 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('not.exist');
});

it('should test workflow with specific trigger node', () => {
cy.createFixtureWorkflow('Two_schedule_triggers.json');

getZoomToFitButton().click();
getExecuteWorkflowButton('Trigger A').should('not.be.visible');
getExecuteWorkflowButton('Trigger B').should('not.be.visible');

// Execute the workflow from trigger A
getNodeByName('Trigger A').realHover();
getExecuteWorkflowButton('Trigger A').should('be.visible');
getExecuteWorkflowButton('Trigger B').should('not.be.visible');
clickExecuteWorkflowButton('Trigger A');

// Check the output
successToast().contains('Workflow executed successfully');
openNode('Edit Fields');
getOutputTableRow(1).should('include.text', 'Trigger A');

clickGetBackToCanvas();
getNdvContainer().should('not.be.visible');

// Execute the workflow from trigger B
getNodeByName('Trigger B').realHover();
getExecuteWorkflowButton('Trigger A').should('not.be.visible');
getExecuteWorkflowButton('Trigger B').should('be.visible');
clickExecuteWorkflowButton('Trigger B');

// Check the output
successToast().contains('Workflow executed successfully');
openNode('Edit Fields');
getOutputTableRow(1).should('include.text', 'Trigger B');
});

describe('execution preview', () => {
it('when deleting the last execution, it should show empty state', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
Expand Down
76 changes: 76 additions & 0 deletions cypress/fixtures/Two_schedule_triggers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6a8c3d85-26f8-4f28-ace9-55a196a23d37",
"name": "prevNode",
"value": "={{ $prevNode.name }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [200, -100],
"id": "351ce967-0399-4a78-848a-9cc69b831796",
"name": "Edit Fields"
},
{
"parameters": {
"rule": {
"interval": [{}]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, -100],
"id": "cf2f58a8-1fbb-4c70-b2b1-9e06bee7ec47",
"name": "Trigger A"
},
{
"parameters": {
"rule": {
"interval": [{}]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 100],
"id": "4fade34e-2bfc-4a2e-a8ed-03ab2ed9c690",
"name": "Trigger B"
}
],
"connections": {
"Trigger A": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Trigger B": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "0dd4627b77a5a795ab9bf073e5812be94dd8d1a5f012248ef2a4acac09be12cb"
}
}
21 changes: 21 additions & 0 deletions packages/cli/src/__tests__/manual-execution.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,26 @@ describe('ManualExecutionService', () => {
name: 'node2',
});
});

it('Should return triggerToStartFrom trigger node', () => {
const data = {
pinData: {
node1: {},
node2: {},
},
triggerToStartFrom: { name: 'node3' },
} as unknown as IWorkflowExecutionDataProcess;
const workflow = {
getNode(nodeName: string) {
return {
name: nodeName,
};
},
} as unknown as Workflow;
const executionStartNode = manualExecutionService.getExecutionStartNode(data, workflow);
expect(executionStartNode).toEqual({
name: 'node3',
});
});
});
});
7 changes: 7 additions & 0 deletions packages/cli/src/manual-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export class ManualExecutionService {

getExecutionStartNode(data: IWorkflowExecutionDataProcess, workflow: Workflow) {
let startNode;

// If the user chose a trigger to start from we honor this.
if (data.triggerToStartFrom?.name) {
startNode = workflow.getNode(data.triggerToStartFrom.name) ?? undefined;
}

// Old logic for partial executions v1
if (
data.startNodes?.length === 1 &&
Object.keys(data.pinData ?? {}).includes(data.startNodes[0].name)
Expand Down
Loading

0 comments on commit b17cbec

Please sign in to comment.