Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (Execute Workflow Node): Inputs for Sub-workflows (#11830) #11837

Merged
merged 42 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ae08d70
feat(Execute Workflow Node): Modify execute workflow parameters (#11830)
igatanasov Nov 21, 2024
7012d9b
feat(Execute Workflow Trigger Node): Add MVP for explicit input param…
CharlieKolb Nov 26, 2024
698f7d0
feat(Execute Workflow Trigger Node): Add types for input fields (#11922)
CharlieKolb Nov 27, 2024
001e3f9
feat(Execute Workflow Trigger Node): Add JSON-based input modes and d…
CharlieKolb Nov 28, 2024
75a571e
Merge branch 'master' of github.com:n8n-io/n8n into feature-sub-workf…
igatanasov Dec 2, 2024
dc400b7
fix(Workflow Trigger Node): Display dropdown for V1.1 and up only (#1…
CharlieKolb Dec 2, 2024
e2b00a3
fix(Workflow Trigger Node): Implement Product feedback (#12008)
CharlieKolb Dec 2, 2024
2e55cbd
feat: Update Example Sub-Workflow (#12000)
CharlieKolb Dec 4, 2024
8e374b4
Merge branch 'master' of github.com:n8n-io/n8n into feature-sub-workf…
igatanasov Dec 5, 2024
c31bf0a
Ado-2898-execute-workflow-node-add-workflow-inputs-parameter (#11887)
igatanasov Dec 6, 2024
b6eb5d1
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 6, 2024
993581c
feat(Sub-Workflow Inputs): Add e2e tests for Sub-Workflow Input (#12076)
CharlieKolb Dec 9, 2024
45dcf99
feat(editor): Add map option to preserve field order (#12084)
igatanasov Dec 9, 2024
c9f2b16
feat(editor): Implement explicit passthrough mode (#12107)
MiloradFilipovic Dec 9, 2024
0cea411
feat(executeWorkflowTrigger): add substitution for workflow input val…
igatanasov Dec 10, 2024
e4d9e7f
Merge branch 'master' of github.com:n8n-io/n8n into feature-sub-workf…
igatanasov Dec 10, 2024
49f204a
feat(core): Implement fixed collection field count validation (#12126)
MiloradFilipovic Dec 10, 2024
0575ddb
feat(editor): Add telemetry for sub-workflow inputs (#12142)
MiloradFilipovic Dec 11, 2024
8c37396
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 11, 2024
d8f003c
Merge remote-tracking branch 'origin/master' into feature-sub-workflo…
netroy Dec 17, 2024
c2ec9d5
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 18, 2024
dd22715
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 18, 2024
c3e874c
fix(editor): Address workflow inputs product feedback (#12284)
MiloradFilipovic Dec 18, 2024
a78ddf7
feat(editor): Implement resource mapping for inputs in workflow tool …
MiloradFilipovic Dec 19, 2024
8351cd6
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 19, 2024
08ae1a4
feat(Execute Workflow Node): Move Type Conversion functionality to Re…
CharlieKolb Dec 19, 2024
142a14e
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 19, 2024
12f1990
Merge remote-tracking branch 'origin/feature-sub-workflow-inputs' int…
MiloradFilipovic Dec 19, 2024
4b5b5f8
chore: Add more e2e Sub-Workflow Input testing (#12308)
CharlieKolb Dec 19, 2024
e09ec8d
chore(Execute Workflow Node): Update text copy for ExecuteWorkflow no…
CharlieKolb Dec 20, 2024
d41b258
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 20, 2024
d2464b5
🔥 Removing duplicate imports
MiloradFilipovic Dec 20, 2024
0616a32
feat(editor): Add stale data warning to Resource Mapper (#12305)
MiloradFilipovic Dec 20, 2024
2ff09cc
fix(Execute Workflow Trigger Node): Make Trigger field required (#12323)
CharlieKolb Dec 20, 2024
ddf4962
chore: Sub-Workflow Inputs PR Pair Review (#12330)
CharlieKolb Dec 20, 2024
259b514
lint
CharlieKolb Dec 20, 2024
a363102
Fix e2e test
CharlieKolb Dec 20, 2024
ef5f400
fix missed lint
CharlieKolb Dec 20, 2024
48cb43e
🔥 Removing legacy subworkflow input from the tool
MiloradFilipovic Dec 20, 2024
ef3bb55
Merge remote-tracking branch 'origin/master' into feature-sub-workflo…
netroy Dec 20, 2024
7b5b93f
Merge branch 'master' into feature-sub-workflow-inputs
MiloradFilipovic Dec 20, 2024
2d4b5f1
Reapply change
CharlieKolb Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions cypress/e2e/48-subworkflow-inputs.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { clickGetBackToCanvas, getOutputTableHeaders } from '../composables/ndv';
import {
clickZoomToFit,
navigateToNewWorkflowPage,
openNode,
pasteWorkflow,
saveWorkflowOnButtonClick,
} from '../composables/workflow';
import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json';
import { NDV, WorkflowsPage, WorkflowPage } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
import { getVisiblePopper } from '../utils';

const ndv = new NDV();
const workflowsPage = new WorkflowsPage();
const workflow = new WorkflowPage();

const DEFAULT_WORKFLOW_NAME = 'My workflow';
const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1';
const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2';

type FieldRow = readonly string[];

const exampleFields = [
['aNumber', 'Number'],
['aString', 'String'],
['aArray', 'Array'],
['aObject', 'Object'],
['aAny', 'Allow Any Type'],
// bool last since it's not an inputField so we'll skip it for some cases
['aBool', 'Boolean'],
] as const;

/**
* Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing
*
* @param items - 2D array of items to populate, i.e. [["myField1", "String"], [""]
* @param collectionName - name of the fixedCollection to populate
* @param offset - amount of 'parameter-input's before the fixedCollection under test
* @returns
*/
function populateFixedCollection(
items: readonly FieldRow[],
collectionName: string,
offset: number,
) {
if (items.length === 0) return;
const n = items[0].length;
for (const [i, params] of items.entries()) {
ndv.actions.addItemToFixedCollection(collectionName);
for (const [j, param] of params.entries()) {
ndv.getters
.fixedCollectionParameter(collectionName)
.getByTestId('parameter-input')
.eq(offset + i * n + j)
.type(`${param}{downArrow}{enter}`);
}
}
}

function makeExample(type: TypeField) {
switch (type) {
case 'String':
return '"example"';
case 'Number':
return '42';
case 'Boolean':
return 'true';
case 'Array':
return '["example", 123, null]';
case 'Object':
return '{{}"example": [123]}';
case 'Allow Any Type':
return 'null';
}
}

type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object';
function populateFields(items: ReadonlyArray<readonly [string, TypeField]>) {
populateFixedCollection(items, 'workflowInputs', 1);
}

function navigateWorkflowSelectionDropdown(index: number, expectedText: string) {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();

getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
getVisiblePopper()
.findChildByTestId('rlc-item')
.eq(index)
.find('span')
.should('have.text', expectedText)
.click();
}

function populateMapperFields(values: readonly string[], offset: number) {
for (const [i, value] of values.entries()) {
cy.getByTestId('parameter-input')
.eq(offset + i)
.type(value);

// Click on a parent to dismiss the pop up hiding the field below.
cy.getByTestId('parameter-input')
.eq(offset + i)
.parent()
.parent()
.click('topLeft');
}
}

// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields
// It then navigates back to the parent and validates output
function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) {
ndv.actions.execute();

// + 1 to account for formatting-only column
getOutputTableHeaders().should('have.length', fields.length + 1);
for (const [i, name] of fields.entries()) {
getOutputTableHeaders().eq(i).should('have.text', name);
}

clickGetBackToCanvas();
saveWorkflowOnButtonClick();

cy.visit(workflowsPage.url);

workflowsPage.getters.workflowCardContent(DEFAULT_WORKFLOW_NAME).click();

openNode('Execute Workflow');

// Note that outside of e2e tests this will be pre-selected correctly.
// Due to our workaround to remain in the same tab we need to select the correct tab manually
navigateWorkflowSelectionDropdown(offset, targetChild);

// This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I <think>
ndv.actions.execute();

getOutputTableHeaders().should('have.length', fields.length + 1);
for (const [i, name] of fields.entries()) {
getOutputTableHeaders().eq(i).should('have.text', name);
}

// todo: verify the fields appear and show the correct types

// todo: fill in the input fields (and mock previous node data in the json fixture to match)

// todo: validate the actual output data
}

function setWorkflowInputFieldValue(index: number, value: string) {
ndv.actions.addItemToFixedCollection('workflowInputs');
ndv.actions.typeIntoFixedCollectionItem('workflowInputs', index, value);
}

describe('Sub-workflow creation and typed usage', () => {
beforeEach(() => {
navigateToNewWorkflowPage();
pasteWorkflow(SUB_WORKFLOW_INPUTS);
saveWorkflowOnButtonClick();
clickZoomToFit();

openNode('Execute Workflow');

// Prevent sub-workflow from opening in new window
cy.window().then((win) => {
cy.stub(win, 'open').callsFake((url) => {
cy.visit(url);
});
});
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');
// **************************
// NAVIGATE TO CHILD WORKFLOW
// **************************

openNode('Workflow Input Trigger');
});

it('works with type-checked values', () => {
populateFields(exampleFields);

validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_1,
1,
exampleFields.map((f) => f[0]),
);

const values = [
'-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it
...exampleFields.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // }} are added automatically
];

// this matches with the pinned data provided in the fixture
populateMapperFields(values, 2);

ndv.actions.execute();

// todo:
// - validate output lines up
// - change input to need casts
// - run
// - confirm error
// - switch `attemptToConvertTypes` flag
// - confirm success and changed output
// - change input to be invalid despite cast
// - run
// - confirm error
// - switch type option flags
// - run
// - confirm success
// - turn off attempt to cast flag
// - confirm a value was not cast
});

it('works with Fields input source into JSON input source', () => {
ndv.getters.nodeOutputHint().should('exist');

populateFields(exampleFields);

validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_1,
1,
exampleFields.map((f) => f[0]),
);

cy.window().then((win) => {
cy.stub(win, 'open').callsFake((url) => {
cy.visit(url);
});
});
navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow');

openNode('Workflow Input Trigger');

cy.getByTestId('parameter-input').eq(0).click();

// Todo: Check if there's a better way to interact with option dropdowns
// This PR would add this child testId
getVisiblePopper()
.getByTestId('parameter-input')
.eq(0)
.type('Using JSON Example{downArrow}{enter}');

const exampleJson =
'{{}' + exampleFields.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
cy.getByTestId('parameter-input-jsonExample')
.find('.cm-line')
.eq(0)
.type(`{selectAll}{backspace}${exampleJson}{enter}`);

// first one doesn't work for some reason, might need to wait for something?
ndv.actions.execute();

validateAndReturnToParent(
DEFAULT_SUBWORKFLOW_NAME_2,
2,
exampleFields.map((f) => f[0]),
);

// test for either InputSource mode and options combinations:
// + we're showing the notice in the output panel
// + we start with no fields
// + Test Step works and we create the fields
// + create field of each type (string, number, boolean, object, array, any)
// + exit ndv
// + save
// + go back to parent workflow
// - verify fields appear [needs Ivan's PR]
// - link fields [needs Ivan's PR]
// + run parent
// - verify output with `null` defaults exists
//
});

it('should show node issue when no fields are defined in manual mode', () => {
ndv.getters.nodeExecuteButton().should('be.disabled');
ndv.actions.close();
// Executing the workflow should show an error toast
workflow.actions.executeWorkflow();
errorToast().should('contain', 'The workflow has issues');
openNode('Workflow Input Trigger');
// Add a field to the workflowInputs fixedCollection
setWorkflowInputFieldValue(0, 'test');
// Executing the workflow should not show error now
ndv.actions.close();
workflow.actions.executeWorkflow();
successToast().should('contain', 'Workflow executed successfully');
});
});
70 changes: 70 additions & 0 deletions cypress/fixtures/Test_Subworkflow-Inputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"meta": {
"instanceId": "4d0676b62208d810ef035130bbfc9fd3afdc78d963ea8ccb9514dc89066efc94"
},
"nodes": [
{
"parameters": {},
"id": "bb7f8bb3-840a-464c-a7de-d3a80538c2be",
"name": "When clicking ‘Test workflow’",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0]
},
{
"parameters": {
"workflowId": {},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"ignoreTypeMismatchErrors": false,
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [500, 240],
"id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453",
"name": "Execute Workflow"
}
],
"connections": {
"When clicking ‘Test workflow’": {
"main": [
[
{
"node": "Execute Workflow",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"When clicking ‘Test workflow’": [
{
"aaString": "A String",
"aaNumber": 1,
"aaArray": [1, true, "3"],
"aaObject": {
"aKey": -1
},
"aaAny": {}
},
{
"aaString": "Another String",
"aaNumber": 2,
"aaArray": [],
"aaObject": {
"aDifferentKey": -1
},
"aaAny": []
}
]
}
}
5 changes: 5 additions & 0 deletions cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ export class NDV extends BasePage {
addItemToFixedCollection: (paramName: string) => {
this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click();
},
typeIntoFixedCollectionItem: (fixedCollectionName: string, index: number, content: string) => {
this.getters.fixedCollectionParameter(fixedCollectionName).within(() => {
cy.getByTestId('parameter-input').eq(index).type(content);
});
},
dragMainPanelToLeft: () => {
cy.drag('[data-test-id=panel-drag-button]', [-1000, 0], { moveTwice: true });
},
Expand Down
Loading
Loading