From a19d4447ac38e40d1fd1da83beb6c20fb7b2d0ed Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:08:51 +0200 Subject: [PATCH] fix(editor): Resolve expressions for grandparent nodes (#5859) * fix(editor): Resolve expressions for grandparent nodes * test: add tests * test: add tests for bug * test: add todos * test: lintfix * test: add small waits * test: add linking tests * test: add test for branch mapping * test: update workflow values * test: comment out test * test: fix up tests with new values * chore: remove todos * test: add ticket number for broken test * test: refactor a bit * test: uncomment * test: fix mapping test * fix: lint issue * test: split tests * Revert "test: split tests" 0290d51d7c983320a718346ccb80fbad93894c6e * test: update mousedown * test: split up tests * test: fix test * test: fix test * test: make less flaky * test: make less flaky * test: enable teset --- cypress/e2e/14-mapping.cy.ts | 2 +- cypress/e2e/24-ndv-paired-item.cy.ts | 288 +++++++++++++++++ cypress/e2e/5-ndv.cy.ts | 1 + cypress/fixtures/Test_workflow_5.json | 292 ++++++++++++++++++ cypress/pages/ndv.ts | 30 ++ .../components/ExpressionParameterInput.vue | 6 +- .../src/components/ParameterInputWrapper.vue | 30 +- packages/editor-ui/src/components/RunData.vue | 13 +- .../editor-ui/src/components/RunDataTable.vue | 1 + .../editor-ui/src/mixins/expressionManager.ts | 19 +- .../editor-ui/src/mixins/workflowHelpers.ts | 79 +---- packages/editor-ui/src/stores/ndv.ts | 9 + packages/editor-ui/src/stores/workflows.ts | 89 +++++- 13 files changed, 759 insertions(+), 100 deletions(-) create mode 100644 cypress/e2e/24-ndv-paired-item.cy.ts create mode 100644 cypress/fixtures/Test_workflow_5.json diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 69487f699e..8c4d6f9cdb 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -205,7 +205,7 @@ describe('Data mapping', () => { 'have.text', `{{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input[0].count }} {{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input }}`, ); - ndv.getters.parameterExpressionPreview('value').should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '[empty]'); ndv.actions.selectInputNode('Set'); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts new file mode 100644 index 0000000000..133cc808bb --- /dev/null +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -0,0 +1,288 @@ +import { WorkflowPage, NDV } from '../pages'; +import { v4 as uuid } from 'uuid'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +describe('NDV', () => { + before(() => { + cy.resetAll(); + cy.skipSetup(); + + }); + beforeEach(() => { + workflowPage.actions.visit(); + workflowPage.actions.renameWorkflow(uuid()); + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + + it('maps paired input and output items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + + workflowPage.actions.openNode('Item Lists'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputPanel().contains('6 items').should('exist'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + // input to output + ndv.getters.inputTableRow(1) + .should('exist') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(1) + .realHover(); + ndv.getters.outputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(2) + .realHover(); + ndv.getters.outputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(3) + .realHover(); + ndv.getters.outputTableRow(6) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + // output to input + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(4) + .realHover(); + ndv.getters.inputTableRow(1) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .realHover(); + ndv.getters.inputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(6) + .realHover(); + ndv.getters.inputTableRow(3) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('maps paired input and output items based on selected input node', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + cy.wait(50); + + ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + + ndv.actions.selectInputNode('Set1'); + ndv.getters.inputHoveringItem().should('have.text', '1000').realHover(); + ndv.getters.outputHoveringItem().should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Item Lists'); + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputHoveringItem().should('have.text', '1111').realHover(); + ndv.getters.outputHoveringItem().should('have.text', '1111'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + }); + + it('maps paired input and output items based on selected run', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set3'); + + ndv.getters.inputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + ndv.getters.outputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + + ndv.getters.outputTableRow(3) + .should('have.text', '4444') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '4444') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.actions.changeOutputRunSelector('2 of 2 (6 items)'); + cy.wait(50); + + ndv.getters.inputTableRow(1) + .should('have.text', '1000') + .realHover(); + ndv.getters.outputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(3) + .should('have.text', '2000') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '2000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('resolves expression with default item when input node is not parent, while still pairing items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.inputHoveringItem().should('have.text', '1111').realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + + ndv.actions.selectInputNode('Code1'); + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Code'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '6666') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('When clicking'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1).should('have.text', "This is an item, but it's empty.").realHover(); + ndv.getters.outputHoveringItem().should('have.length', 6); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + }); + + it('can pair items between input and output across branches and runs', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('IF'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.switchOutputBranch('False Branch (2 items)'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputTableRow(5) + .should('have.text', '8888') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .should('have.text', '9999') + .realHover(); + ndv.getters.inputTableRow(6) + .should('have.text', '9999') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.actions.close(); + workflowPage.actions.openNode('Set5'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputHoveringItem().should('not.exist'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + + ndv.actions.switchIntputBranch('False Branch'); + ndv.getters.inputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.outputHoveringItem().should('have.text', '8888'); + + ndv.actions.changeOutputRunSelector('1 of 2 (4 items)') + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + // todo there's a bug here need to fix ADO-534 + // ndv.getters.outputHoveringItem().should('not.exist'); + }); +}); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index ea4533725c..c9c82e50fa 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -15,6 +15,7 @@ describe('NDV', () => { workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); }); + it('should show up when double clicked on a node and close when Back to canvas clicked', () => { workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.getters.canvasNodes().first().dblclick(); diff --git a/cypress/fixtures/Test_workflow_5.json b/cypress/fixtures/Test_workflow_5.json new file mode 100644 index 0000000000..6b87fc33b7 --- /dev/null +++ b/cypress/fixtures/Test_workflow_5.json @@ -0,0 +1,292 @@ +{ + "meta": { + "instanceId": "8147b3a74cd161276e0f3bfc17369a724afab0d377593fada8be82d34c0c6a95" + }, + "nodes": [ + { + "parameters": { + "jsCode": "return [\n {\n id: 6666\n },\n {\n id: 3333\n },\n {\n id: 9999\n },\n {\n id: 1111\n },\n {\n id: 4444\n },\n {\n id: 8888\n },\n]" + }, + "id": "5f023c7c-67ca-47a0-8a90-8227fcf29b9c", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "bd454282-9dd7-465f-9b9a-654a0c8532ec", + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -40, + 780 + ] + }, + { + "parameters": {}, + "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -740, + 580 + ] + }, + { + "parameters": { + "operation": "sort", + "sortFieldsUi": { + "sortField": [ + { + "fieldName": "id" + } + ] + }, + "options": {} + }, + "id": "555a150c-d735-4331-b628-c1f1cfed2da1", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [ + -280, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "02372cb6-aac8-45c3-8600-f699901289ac", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -60, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "00d73944-218c-4896-af68-3f2855a922d1", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -280, + 780 + ] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.id }}", + "operation": "smallerEqual", + "value2": 6666 + } + ] + } + }, + "id": "211a7bef-32d1-4928-9cef-3a45f2e61379", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 160, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "dcbd4745-832f-43d8-8a3c-dd80e8ca2777", + "name": "Set3", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 140, + 780 + ] + }, + { + "parameters": { + "jsCode": "return [\n {\n id: 1000\n },\n {\n id: 300\n },\n {\n id: 2000\n },\n {\n id: 100\n },\n {\n id: 400\n },\n {\n id: 1300\n },\n]" + }, + "id": "ec9c8f16-f3c8-4054-a6e9-4f1ebcdebb71", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 780 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "42e89478-a53a-4d10-b20c-1dc5d5f953d5", + "name": "Set4", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "5085eb1c-0345-4b9d-856a-2955279f2c5d", + "name": "Set5", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 660 + ] + } + ], + "connections": { + "Code": { + "main": [ + [ + { + "node": "Item Lists", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set2": { + "main": [ + [ + { + "node": "Set3", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + }, + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set1": { + "main": [ + [ + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Set4", + "type": "main", + "index": 0 + }, + { + "node": "Set5", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Set5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + } + } +} \ No newline at end of file diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 7ce0c811fe..15f177240f 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -45,6 +45,12 @@ export class NDV extends BasePage { executePrevious: () => cy.getByTestId('execute-previous-node'), httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), + inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'), + outputRunSelector: () => this.getters.outputPanel().findChildByTestId('run-selector'), + outputHoveringItem: () => this.getters.outputPanel().findChildByTestId('hovering-item'), + inputHoveringItem: () => this.getters.inputPanel().findChildByTestId('hovering-item'), + outputBranches: () => this.getters.outputPanel().findChildByTestId('branches'), + inputBranches: () => this.getters.inputPanel().findChildByTestId('branches'), }; actions = { @@ -119,5 +125,29 @@ export class NDV extends BasePage { this.actions.editPinnedData(); this.actions.savePinnedData(); }, + changeInputRunSelector: (runName: string) => { + this.getters.inputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + changeOutputRunSelector: (runName: string) => { + this.getters.outputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + toggleOutputRunLinking: () => { + this.getters.outputRunSelector().find('button').click(); + }, + toggleInputRunLinking: () => { + this.getters.inputRunSelector().find('button').click(); + }, + switchOutputBranch: (name: string) => { + this.getters.outputBranches().get('span').contains(name).click(); + }, + switchIntputBranch: (name: string) => { + this.getters.inputBranches().get('span').contains(name).click(); + }, }; } diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index 51e58eef9d..3a8b6e79a5 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -115,7 +115,11 @@ export default Vue.extend({ return (this.hoveringItem?.itemIndex ?? 0) + 1; }, hoveringItem(): TargetItem | null { - return this.ndvStore.hoveringItem; + if (this.ndvStore.isInputParentOfActiveNode) { + return this.ndvStore.hoveringItem; + } + + return null; }, isDragging(): boolean { return this.ndvStore.isDraggableDragging; diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index d1f2c73ebb..86e6ad9acb 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -28,7 +28,7 @@ :class="$style.hint" data-test-id="parameter-expression-preview" class="ph-no-capture" - :highlight="!!(expressionOutput && targetItem)" + :highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode" :hint="expressionOutput" :singleLine="true" /> @@ -153,25 +153,29 @@ export default mixins(showMessage, workflowHelpers).extend({ targetItem(): TargetItem | null { return this.ndvStore.hoveringItem; }, + isInputParentOfActiveNode(): boolean { + return this.ndvStore.isInputParentOfActiveNode; + }, expressionValueComputed(): string | null { - const inputNodeName: string | undefined = this.ndvStore.ndvInputNodeName; const value = isResourceLocatorValue(this.value) ? this.value.value : this.value; - if (this.activeNode === null || !this.isValueExpression || typeof value !== 'string') { + if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') { return null; } - const inputRunIndex: number | undefined = this.ndvStore.ndvInputRunIndex; - const inputBranchIndex: number | undefined = this.ndvStore.ndvInputBranchIndex; - let computedValue: NodeParameterValue; try { - const targetItem = this.targetItem ?? undefined; - computedValue = this.resolveExpression(value, undefined, { - targetItem, - inputNodeName, - inputRunIndex, - inputBranchIndex, - }); + let opts; + if (this.ndvStore.isInputParentOfActiveNode) { + opts = { + targetItem: this.targetItem ?? undefined, + inputNodeName: this.ndvStore.ndvInputNodeName, + inputRunIndex: this.ndvStore.ndvInputRunIndex, + inputBranchIndex: this.ndvStore.ndvInputBranchIndex, + }; + } + + computedValue = this.resolveExpression(value, undefined, opts); + if (computedValue === null) { return null; } diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index eb31cf17b1..8a97746285 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -121,7 +121,12 @@ -
+
-
+
diff --git a/packages/editor-ui/src/components/RunDataTable.vue b/packages/editor-ui/src/components/RunDataTable.vue index 07cd4d134d..453f71cf3d 100644 --- a/packages/editor-ui/src/components/RunDataTable.vue +++ b/packages/editor-ui/src/components/RunDataTable.vue @@ -107,6 +107,7 @@ v-for="(row, index1) in tableData.data" :key="index1" :class="{ [$style.hoveringRow]: isHoveringRow(index1) }" + :data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined" > => {}, - // @ts-ignore - getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { - const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); - - if (nodeTypeDescription === null) { - return undefined; - } - - return { - description: nodeTypeDescription, - // As we do not have the trigger/poll functions available in the frontend - // we use the information available to figure out what are trigger nodes - // @ts-ignore - trigger: - (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && - nodeTypeDescription.inputs.length === 0 && - !nodeTypeDescription.webhooks) || - undefined, - }; - }, - }; - - return nodeTypes; + return useWorkflowsStore().getNodeTypes(); } // Returns connectionInputData to be able to execute an expression. diff --git a/packages/editor-ui/src/stores/ndv.ts b/packages/editor-ui/src/stores/ndv.ts index fed4fc96ea..caf76b11d9 100644 --- a/packages/editor-ui/src/stores/ndv.ts +++ b/packages/editor-ui/src/stores/ndv.ts @@ -111,6 +111,15 @@ export const useNDVStore = defineStore(STORES.NDV, { isDNVDataEmpty() { return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty; }, + isInputParentOfActiveNode(): boolean { + const inputNodeName = this.ndvInputNodeName; + if (!this.activeNode || !inputNodeName) { + return false; + } + const workflow = useWorkflowsStore().getCurrentWorkflow(); + const parentNodes = workflow.getParentNodes(this.activeNode.name, 'main', 1); + return parentNodes.includes(inputNodeName); + }, }, actions: { setInputNodeName(name: string | undefined): void { diff --git a/packages/editor-ui/src/stores/workflows.ts b/packages/editor-ui/src/stores/workflows.ts index 2b1853d149..3d9600c0c8 100644 --- a/packages/editor-ui/src/stores/workflows.ts +++ b/packages/editor-ui/src/stores/workflows.ts @@ -2,8 +2,10 @@ import { DEFAULT_NEW_WORKFLOW_NAME, DUPLICATE_POSTFFIX, EnterpriseEditionFeature, + ERROR_TRIGGER_NODE_TYPE, MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID, + START_NODE_TYPE, STORES, } from '@/constants'; import { @@ -36,6 +38,8 @@ import { INodeExecutionData, INodeIssueData, INodeParameters, + INodeTypeData, + INodeTypes, IPinData, IRun, IRunData, @@ -43,6 +47,7 @@ import { ITaskData, IWorkflowSettings, NodeHelpers, + Workflow, } from 'n8n-workflow'; import Vue from 'vue'; @@ -86,6 +91,9 @@ const createEmptyWorkflow = (): IWorkflowDb => ({ usedCredentials: [], }); +let cachedWorkflowKey: string | null = ''; +let cachedWorkflow: Workflow | null = null; + export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { state: (): WorkflowsState => ({ workflow: createEmptyWorkflow(), @@ -256,7 +264,86 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, }, actions: { - // Workflow actions + getNodeTypes(): INodeTypes { + const nodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (nodeTypes?: INodeTypeData): Promise => {}, + // @ts-ignore + getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { + const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); + + if (nodeTypeDescription === null) { + return undefined; + } + + return { + description: nodeTypeDescription, + // As we do not have the trigger/poll functions available in the frontend + // we use the information available to figure out what are trigger nodes + // @ts-ignore + trigger: + (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && + nodeTypeDescription.inputs.length === 0 && + !nodeTypeDescription.webhooks) || + undefined, + }; + }, + }; + + return nodeTypes; + }, + + // Returns a shallow copy of the nodes which means that all the data on the lower + // levels still only gets referenced but the top level object is a different one. + // This has the advantage that it is very fast and does not cause problems with vuex + // when the workflow replaces the node-parameters. + getNodes(): INodeUi[] { + const nodes = useWorkflowsStore().allNodes; + const returnNodes: INodeUi[] = []; + + for (const node of nodes) { + returnNodes.push(Object.assign({}, node)); + } + + return returnNodes; + }, + + // Returns a workflow instance. + getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { + const nodeTypes = this.getNodeTypes(); + let workflowId: string | undefined = useWorkflowsStore().workflowId; + if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + workflowId = undefined; + } + + const workflowName = useWorkflowsStore().workflowName; + + cachedWorkflow = new Workflow({ + id: workflowId, + name: workflowName, + nodes: copyData ? deepCopy(nodes) : nodes, + connections: copyData ? deepCopy(connections) : connections, + active: false, + nodeTypes, + settings: useWorkflowsStore().workflowSettings, + // @ts-ignore + pinData: useWorkflowsStore().getPinData, + }); + + return cachedWorkflow; + }, + + getCurrentWorkflow(copyData?: boolean): Workflow { + const nodes = this.getNodes(); + const connections = this.allConnections; + const cacheKey = JSON.stringify({ nodes, connections }); + if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { + return cachedWorkflow; + } + cachedWorkflowKey = cacheKey; + + return this.getWorkflow(nodes, connections, copyData); + }, async fetchAllWorkflows(): Promise { const rootStore = useRootStore();