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"

0290d51d7c

* test: update mousedown

* test: split up tests

* test: fix test

* test: fix test

* test: make less flaky

* test: make less flaky

* test: enable teset
This commit is contained in:
Mutasem Aldmour 2023-04-21 14:08:51 +02:00 committed by GitHub
parent d17d050a16
commit a19d4447ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 759 additions and 100 deletions

View file

@ -205,7 +205,7 @@ describe('Data mapping', () => {
'have.text', 'have.text',
`{{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input[0].count }} {{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input }}`, `{{ $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'); ndv.actions.selectInputNode('Set');

View file

@ -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');
});
});

View file

@ -15,6 +15,7 @@ describe('NDV', () => {
workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.renameWorkflow(uuid());
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
}); });
it('should show up when double clicked on a node and close when Back to canvas clicked', () => { it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();

View file

@ -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
}
]
]
}
}
}

View file

@ -45,6 +45,12 @@ export class NDV extends BasePage {
executePrevious: () => cy.getByTestId('execute-previous-node'), executePrevious: () => cy.getByTestId('execute-previous-node'),
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), 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 = { actions = {
@ -119,5 +125,29 @@ export class NDV extends BasePage {
this.actions.editPinnedData(); this.actions.editPinnedData();
this.actions.savePinnedData(); 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();
},
}; };
} }

View file

@ -115,7 +115,11 @@ export default Vue.extend({
return (this.hoveringItem?.itemIndex ?? 0) + 1; return (this.hoveringItem?.itemIndex ?? 0) + 1;
}, },
hoveringItem(): TargetItem | null { hoveringItem(): TargetItem | null {
return this.ndvStore.hoveringItem; if (this.ndvStore.isInputParentOfActiveNode) {
return this.ndvStore.hoveringItem;
}
return null;
}, },
isDragging(): boolean { isDragging(): boolean {
return this.ndvStore.isDraggableDragging; return this.ndvStore.isDraggableDragging;

View file

@ -28,7 +28,7 @@
:class="$style.hint" :class="$style.hint"
data-test-id="parameter-expression-preview" data-test-id="parameter-expression-preview"
class="ph-no-capture" class="ph-no-capture"
:highlight="!!(expressionOutput && targetItem)" :highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode"
:hint="expressionOutput" :hint="expressionOutput"
:singleLine="true" :singleLine="true"
/> />
@ -153,25 +153,29 @@ export default mixins(showMessage, workflowHelpers).extend({
targetItem(): TargetItem | null { targetItem(): TargetItem | null {
return this.ndvStore.hoveringItem; return this.ndvStore.hoveringItem;
}, },
isInputParentOfActiveNode(): boolean {
return this.ndvStore.isInputParentOfActiveNode;
},
expressionValueComputed(): string | null { expressionValueComputed(): string | null {
const inputNodeName: string | undefined = this.ndvStore.ndvInputNodeName;
const value = isResourceLocatorValue(this.value) ? this.value.value : this.value; 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; return null;
} }
const inputRunIndex: number | undefined = this.ndvStore.ndvInputRunIndex;
const inputBranchIndex: number | undefined = this.ndvStore.ndvInputBranchIndex;
let computedValue: NodeParameterValue; let computedValue: NodeParameterValue;
try { try {
const targetItem = this.targetItem ?? undefined; let opts;
computedValue = this.resolveExpression(value, undefined, { if (this.ndvStore.isInputParentOfActiveNode) {
targetItem, opts = {
inputNodeName, targetItem: this.targetItem ?? undefined,
inputRunIndex, inputNodeName: this.ndvStore.ndvInputNodeName,
inputBranchIndex, inputRunIndex: this.ndvStore.ndvInputRunIndex,
}); inputBranchIndex: this.ndvStore.ndvInputBranchIndex,
};
}
computedValue = this.resolveExpression(value, undefined, opts);
if (computedValue === null) { if (computedValue === null) {
return null; return null;
} }

View file

@ -121,7 +121,12 @@
</div> </div>
</div> </div>
<div :class="$style.runSelector" v-if="maxRunIndex > 0" v-show="!editMode.enabled"> <div
:class="$style.runSelector"
v-if="maxRunIndex > 0"
v-show="!editMode.enabled"
data-test-id="run-selector"
>
<n8n-select <n8n-select
size="small" size="small"
:value="runIndex" :value="runIndex"
@ -157,7 +162,11 @@
<slot name="run-info"></slot> <slot name="run-info"></slot>
</div> </div>
<div v-if="maxOutputIndex > 0 && branches.length > 1" :class="$style.tabs"> <div
v-if="maxOutputIndex > 0 && branches.length > 1"
:class="$style.tabs"
data-test-id="branches"
>
<n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" /> <n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" />
</div> </div>

View file

@ -107,6 +107,7 @@
v-for="(row, index1) in tableData.data" v-for="(row, index1) in tableData.data"
:key="index1" :key="index1"
:class="{ [$style.hoveringRow]: isHoveringRow(index1) }" :class="{ [$style.hoveringRow]: isHoveringRow(index1) }"
:data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined"
> >
<td <td
v-for="(data, index2) in row" v-for="(data, index2) in row"

View file

@ -170,16 +170,21 @@ export const expressionManager = mixins(workflowHelpers).extend({
}; };
try { try {
if (!useNDVStore().activeNode) { const ndvStore = useNDVStore();
if (!ndvStore.activeNode) {
// e.g. credential modal // e.g. credential modal
result.resolved = Expression.resolveWithoutWorkflow(resolvable); result.resolved = Expression.resolveWithoutWorkflow(resolvable);
} else { } else {
result.resolved = this.resolveExpression('=' + resolvable, undefined, { let opts;
targetItem: targetItem ?? undefined, if (ndvStore.isInputParentOfActiveNode) {
inputNodeName: this.ndvStore.ndvInputNodeName, opts = {
inputRunIndex: this.ndvStore.ndvInputRunIndex, targetItem: targetItem ?? undefined,
inputBranchIndex: this.ndvStore.ndvInputBranchIndex, inputNodeName: this.ndvStore.ndvInputNodeName,
}); inputRunIndex: this.ndvStore.ndvInputRunIndex,
inputBranchIndex: this.ndvStore.ndvInputBranchIndex,
};
}
result.resolved = this.resolveExpression('=' + resolvable, undefined, opts);
} }
} catch (error) { } catch (error) {
result.resolved = `[${error.message}]`; result.resolved = `[${error.message}]`;

View file

@ -67,9 +67,6 @@ import { getWorkflowPermissions, IPermissions } from '@/permissions';
import { ICredentialsResponse } from '@/Interface'; import { ICredentialsResponse } from '@/Interface';
import { useEnvironmentsStore } from '@/stores'; import { useEnvironmentsStore } from '@/stores';
let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null;
export function resolveParameter( export function resolveParameter(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
opts: { opts: {
@ -89,10 +86,6 @@ export function resolveParameter(
let parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); let parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
const executionData = useWorkflowsStore().getWorkflowExecution; const executionData = useWorkflowsStore().getWorkflowExecution;
if (opts?.inputNodeName && !parentNode.includes(opts.inputNodeName)) {
return null;
}
let runIndexParent = opts?.inputRunIndex ?? 0; let runIndexParent = opts?.inputRunIndex ?? 0;
const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]); const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]);
if (opts.targetItem && opts?.targetItem?.nodeName === activeNode!.name && executionData) { if (opts.targetItem && opts?.targetItem?.nodeName === activeNode!.name && executionData) {
@ -184,84 +177,20 @@ export function resolveParameter(
} }
function getCurrentWorkflow(copyData?: boolean): Workflow { function getCurrentWorkflow(copyData?: boolean): Workflow {
const nodes = getNodes(); return useWorkflowsStore().getCurrentWorkflow(copyData);
const connections = useWorkflowsStore().allConnections;
const cacheKey = JSON.stringify({ nodes, connections });
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
return cachedWorkflow;
}
cachedWorkflowKey = cacheKey;
return getWorkflow(nodes, connections, copyData);
} }
// 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.
function getNodes(): INodeUi[] { function getNodes(): INodeUi[] {
const nodes = useWorkflowsStore().allNodes; return useWorkflowsStore().getNodes();
const returnNodes: INodeUi[] = [];
for (const node of nodes) {
returnNodes.push(Object.assign({}, node));
}
return returnNodes;
} }
// Returns a workflow instance. // Returns a workflow instance.
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
const nodeTypes = getNodeTypes(); return useWorkflowsStore().getWorkflow(nodes, connections, copyData);
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;
} }
function getNodeTypes(): INodeTypes { function getNodeTypes(): INodeTypes {
const nodeTypes: INodeTypes = { return useWorkflowsStore().getNodeTypes();
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => {},
// @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 connectionInputData to be able to execute an expression. // Returns connectionInputData to be able to execute an expression.

View file

@ -111,6 +111,15 @@ export const useNDVStore = defineStore(STORES.NDV, {
isDNVDataEmpty() { isDNVDataEmpty() {
return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty; 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: { actions: {
setInputNodeName(name: string | undefined): void { setInputNodeName(name: string | undefined): void {

View file

@ -2,8 +2,10 @@ import {
DEFAULT_NEW_WORKFLOW_NAME, DEFAULT_NEW_WORKFLOW_NAME,
DUPLICATE_POSTFFIX, DUPLICATE_POSTFFIX,
EnterpriseEditionFeature, EnterpriseEditionFeature,
ERROR_TRIGGER_NODE_TYPE,
MAX_WORKFLOW_NAME_LENGTH, MAX_WORKFLOW_NAME_LENGTH,
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
START_NODE_TYPE,
STORES, STORES,
} from '@/constants'; } from '@/constants';
import { import {
@ -36,6 +38,8 @@ import {
INodeExecutionData, INodeExecutionData,
INodeIssueData, INodeIssueData,
INodeParameters, INodeParameters,
INodeTypeData,
INodeTypes,
IPinData, IPinData,
IRun, IRun,
IRunData, IRunData,
@ -43,6 +47,7 @@ import {
ITaskData, ITaskData,
IWorkflowSettings, IWorkflowSettings,
NodeHelpers, NodeHelpers,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import Vue from 'vue'; import Vue from 'vue';
@ -86,6 +91,9 @@ const createEmptyWorkflow = (): IWorkflowDb => ({
usedCredentials: [], usedCredentials: [],
}); });
let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null;
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
state: (): WorkflowsState => ({ state: (): WorkflowsState => ({
workflow: createEmptyWorkflow(), workflow: createEmptyWorkflow(),
@ -256,7 +264,86 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
}, },
}, },
actions: { actions: {
// Workflow actions getNodeTypes(): INodeTypes {
const nodeTypes: INodeTypes = {
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => {},
// @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<IWorkflowDb[]> { async fetchAllWorkflows(): Promise<IWorkflowDb[]> {
const rootStore = useRootStore(); const rootStore = useRootStore();