feat(editor): update expressions display (#4171)

* N8n 4673 expressions res1 (#4149)

* hide hints if necessary

* refactor out parameter input

* refactor param input in creds

* remove any

* add expression result before

* update case

* add types

* fix spacing

* update types

* update expr

* update parameter input

* update param input

* update param input

* remove import

* fix typo

* update value

* fix drop for rl

* add state to track hovering item

* add hover behavior to resolve values

* update index

* fix run selector bug

* add run item to eval expr

* add paired item mappings

* fix rec bug

* Fix for loops

* handle pinned data

* add missing pinned

* fix bug

* support parent

* add input

* map back from output

* clean up

* fix output bug

* fix branching bug

* update preview

* only if expr

* fix output

* fix expr eval for outputs

* add default hover state

* fix hover state

* fix branching

* hide hint if expr

* remove duplicate logic

* update style

* allow opening expr in demo

* update expr

* update row hover

* update param name

* clean up

* update hovering state

* update default output

* fix duplicate import

* update hover behavior

* update package lock

* fix pinned data case

* address case when no input
This commit is contained in:
Mutasem Aldmour
2022-10-12 14:06:28 +02:00
committed by GitHub
parent fe7c8a85ce
commit 6b538494ce
28 changed files with 842 additions and 228 deletions

106
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.196.0", "version": "0.197.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n8n", "name": "n8n",
"version": "0.196.0", "version": "0.197.1",
"hasInstallScript": true, "hasInstallScript": true,
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
@@ -43257,7 +43257,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "n8n", "name": "n8n",
"version": "0.196.0", "version": "0.197.1",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@@ -43303,10 +43303,10 @@
"lodash.split": "^4.4.2", "lodash.split": "^4.4.2",
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"n8n-editor-ui": "~0.162.0", "n8n-editor-ui": "~0.163.1",
"n8n-nodes-base": "~0.194.0", "n8n-nodes-base": "~0.195.1",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
@@ -44453,7 +44453,7 @@
}, },
"packages/core": { "packages/core": {
"name": "n8n-core", "name": "n8n-core",
"version": "0.136.0", "version": "0.137.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@@ -44465,7 +44465,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"qs": "^6.10.1", "qs": "^6.10.1",
@@ -45521,7 +45521,7 @@
}, },
"packages/design-system": { "packages/design-system": {
"name": "n8n-design-system", "name": "n8n-design-system",
"version": "0.36.0", "version": "0.37.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"element-ui": "~2.15.7", "element-ui": "~2.15.7",
@@ -45704,7 +45704,7 @@
}, },
"packages/editor-ui": { "packages/editor-ui": {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.162.0", "version": "0.163.1",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@fontsource/open-sans": "^4.5.0", "@fontsource/open-sans": "^4.5.0",
@@ -45728,8 +45728,8 @@
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"luxon": "^2.3.0", "luxon": "^2.3.0",
"monaco-editor": "^0.30.1", "monaco-editor": "^0.30.1",
"n8n-design-system": "~0.36.0", "n8n-design-system": "~0.37.0",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "2.0.0-dev.4", "quill": "2.0.0-dev.4",
@@ -46156,7 +46156,7 @@
}, },
"packages/node-dev": { "packages/node-dev": {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "0.75.0", "version": "0.76.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@@ -46164,8 +46164,8 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
@@ -46185,21 +46185,9 @@
"@types/vorpal": "^1.11.0" "@types/vorpal": "^1.11.0"
} }
}, },
"packages/node-dev/node_modules/typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"packages/nodes-base": { "packages/nodes-base": {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "0.194.0", "version": "0.195.1",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@kafkajs/confluent-schema-registry": "1.0.6", "@kafkajs/confluent-schema-registry": "1.0.6",
@@ -46237,7 +46225,7 @@
"mqtt": "4.2.6", "mqtt": "4.2.6",
"mssql": "^8.1.2", "mssql": "^8.1.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"node-html-markdown": "^1.1.3", "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0", "node-ssh": "^12.0.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
@@ -46292,7 +46280,7 @@
"eslint-plugin-n8n-nodes-base": "^1.9.3", "eslint-plugin-n8n-nodes-base": "^1.9.3",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"jest": "^27.4.7", "jest": "^27.4.7",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~4.8.0" "typescript": "~4.8.0"
@@ -47329,7 +47317,7 @@
}, },
"packages/workflow": { "packages/workflow": {
"name": "n8n-workflow", "name": "n8n-workflow",
"version": "0.118.0", "version": "0.119.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@n8n_io/riot-tmpl": "^1.0.1", "@n8n_io/riot-tmpl": "^1.0.1",
@@ -52127,7 +52115,7 @@
"@oclif/errors": "^1.3.5", "@oclif/errors": "^1.3.5",
"@oclif/parser": "^3.8.0", "@oclif/parser": "^3.8.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"globby": "^11.0.2", "globby": "^11.0.1",
"is-wsl": "^2.1.1", "is-wsl": "^2.1.1",
"tslib": "^2.3.1" "tslib": "^2.3.1"
}, },
@@ -52153,10 +52141,10 @@
"clean-stack": "^3.0.1", "clean-stack": "^3.0.1",
"cli-progress": "^3.10.0", "cli-progress": "^3.10.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ejs": "^3.1.8", "ejs": "^3.1.6",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"get-package-type": "^0.1.0", "get-package-type": "^0.1.0",
"globby": "^11.0.2", "globby": "^11.1.0",
"hyperlinker": "^1.0.0", "hyperlinker": "^1.0.0",
"indent-string": "^4.0.0", "indent-string": "^4.0.0",
"is-wsl": "^2.2.0", "is-wsl": "^2.2.0",
@@ -52368,7 +52356,7 @@
"@oclif/errors": "^1.3.3", "@oclif/errors": "^1.3.3",
"@oclif/parser": "^3.8.0", "@oclif/parser": "^3.8.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"globby": "^11.0.2", "globby": "^11.0.1",
"is-wsl": "^2.1.1", "is-wsl": "^2.1.1",
"tslib": "^2.0.0" "tslib": "^2.0.0"
} }
@@ -52561,7 +52549,7 @@
"@oclif/errors": "^1.3.3", "@oclif/errors": "^1.3.3",
"@oclif/parser": "^3.8.0", "@oclif/parser": "^3.8.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"globby": "^11.0.2", "globby": "^11.0.1",
"is-wsl": "^2.1.1", "is-wsl": "^2.1.1",
"tslib": "^2.0.0" "tslib": "^2.0.0"
} }
@@ -53067,7 +53055,7 @@
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"find-up": "^5.0.0", "find-up": "^5.0.0",
"fork-ts-checker-webpack-plugin": "^6.0.4", "fork-ts-checker-webpack-plugin": "^4.1.6",
"glob": "^7.1.6", "glob": "^7.1.6",
"glob-promise": "^3.4.0", "glob-promise": "^3.4.0",
"global": "^4.4.0", "global": "^4.4.0",
@@ -55589,7 +55577,7 @@
"@typescript-eslint/types": "5.38.1", "@typescript-eslint/types": "5.38.1",
"@typescript-eslint/visitor-keys": "5.38.1", "@typescript-eslint/visitor-keys": "5.38.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.0.2", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"semver": "^7.3.7", "semver": "^7.3.7",
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
@@ -56959,7 +56947,7 @@
"integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==",
"dev": true, "dev": true,
"requires": { "requires": {
"browserslist": "^4.21.3", "browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001109", "caniuse-lite": "^1.0.30001109",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"num2fraction": "^1.2.2", "num2fraction": "^1.2.2",
@@ -59784,7 +59772,7 @@
"integrity": "sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ==", "integrity": "sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"browserslist": "^4.21.3" "browserslist": "^4.21.4"
} }
}, },
"core-js-pure": { "core-js-pure": {
@@ -59858,7 +59846,7 @@
"requires": { "requires": {
"arrify": "^2.0.1", "arrify": "^2.0.1",
"cp-file": "^7.0.0", "cp-file": "^7.0.0",
"globby": "^11.0.2", "globby": "^9.2.0",
"has-glob": "^1.0.0", "has-glob": "^1.0.0",
"junk": "^3.1.0", "junk": "^3.1.0",
"nested-error-stacks": "^2.1.0", "nested-error-stacks": "^2.1.0",
@@ -61462,7 +61450,7 @@
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"glob-parent": "^6.0.1", "glob-parent": "^6.0.1",
"globals": "^13.15.0", "globals": "^13.15.0",
"globby": "^11.0.2", "globby": "^11.1.0",
"grapheme-splitter": "^1.0.4", "grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"import-fresh": "^3.0.0", "import-fresh": "^3.0.0",
@@ -71914,10 +71902,10 @@
"lodash.split": "^4.4.2", "lodash.split": "^4.4.2",
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"n8n-editor-ui": "~0.162.0", "n8n-editor-ui": "~0.163.1",
"n8n-nodes-base": "~0.194.0", "n8n-nodes-base": "~0.195.1",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
@@ -72807,7 +72795,7 @@
"jest": "^27.4.7", "jest": "^27.4.7",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"qs": "^6.10.1", "qs": "^6.10.1",
@@ -73791,8 +73779,8 @@
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"luxon": "^2.3.0", "luxon": "^2.3.0",
"monaco-editor": "^0.30.1", "monaco-editor": "^0.30.1",
"n8n-design-system": "~0.36.0", "n8n-design-system": "~0.37.0",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "2.0.0-dev.4", "quill": "2.0.0-dev.4",
@@ -74109,19 +74097,13 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"tmp-promise": "^3.0.2", "tmp-promise": "^3.0.2",
"typescript": "~4.8.0" "typescript": "~4.8.0"
},
"dependencies": {
"typescript": {
"version": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg=="
}
} }
}, },
"n8n-nodes-base": { "n8n-nodes-base": {
@@ -74194,8 +74176,8 @@
"mqtt": "4.2.6", "mqtt": "4.2.6",
"mssql": "^8.1.2", "mssql": "^8.1.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.136.0", "n8n-core": "~0.137.0",
"n8n-workflow": "~0.118.0", "n8n-workflow": "~0.119.0",
"node-html-markdown": "^1.1.3", "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0", "node-ssh": "^12.0.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
@@ -78476,7 +78458,7 @@
"fs-extra": "^6.0.1", "fs-extra": "^6.0.1",
"get-stream": "^5.1.0", "get-stream": "^5.1.0",
"glob": "^7.1.2", "glob": "^7.1.2",
"globby": "^11.0.2", "globby": "^10.0.1",
"http-call": "^5.1.2", "http-call": "^5.1.2",
"load-json-file": "^6.2.0", "load-json-file": "^6.2.0",
"pkg-dir": "^4.2.0", "pkg-dir": "^4.2.0",
@@ -84173,7 +84155,7 @@
"consola": "^2.15.3", "consola": "^2.15.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"dotenv-expand": "^8.0.2", "dotenv-expand": "^8.0.2",
"ejs": "^3.1.8", "ejs": "^3.1.6",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.11",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"html-minifier-terser": "^6.1.0", "html-minifier-terser": "^6.1.0",

View File

@@ -72,33 +72,34 @@
var(--color-secondary-l) var(--color-secondary-l)
); );
--color-secondary-tint-1-h: 247;
--color-secondary-tint-1-s: 49%;
--color-secondary-tint-1-l: 85%; --color-secondary-tint-1-l: 85%;
--color-secondary-tint-1: hsl( --color-secondary-tint-1: hsl(
var(--color-secondary-tint-1-h), var(--color-secondary-h),
var(--color-secondary-tint-1-s), var(--color-secondary-s),
var(--color-secondary-tint-1-l) var(--color-secondary-tint-1-l)
); );
--color-secondary-tint-2-h: 247;
--color-secondary-tint-2-s: 49%;
--color-secondary-tint-2-l: 92%; --color-secondary-tint-2-l: 92%;
--color-secondary-tint-2: hsl( --color-secondary-tint-2: hsl(
var(--color-secondary-tint-2-h), var(--color-secondary-h),
var(--color-secondary-tint-2-s), var(--color-secondary-s),
var(--color-secondary-tint-2-l) var(--color-secondary-tint-2-l)
); );
--color-secondary-tint-3-h: 247;
--color-secondary-tint-3-s: 49%;
--color-secondary-tint-3-l: 95%; --color-secondary-tint-3-l: 95%;
--color-secondary-tint-3: hsl( --color-secondary-tint-3: hsl(
var(--color-secondary-tint-3-h), var(--color-secondary-h),
var(--color-secondary-tint-3-s), var(--color-secondary-s),
var(--color-secondary-tint-3-l) var(--color-secondary-tint-3-l)
); );
--color-secondary-tint-4-l: 98%;
--color-secondary-tint-4: hsl(
var(--color-secondary-h),
var(--color-secondary-s),
var(--color-secondary-tint-4-l)
);
--color-success-h: 150.4; --color-success-h: 150.4;
--color-success-s: 60%; --color-success-s: 60%;
--color-success-l: 40.4%; --color-success-l: 40.4%;

View File

@@ -31,7 +31,7 @@ import ColorCircles from './ColorCircles.vue';
<Canvas> <Canvas>
<Story name="secondary"> <Story name="secondary">
{{ {{
template: `<color-circles :colors="['--color-secondary', '--color-secondary-tint-1', '--color-secondary-tint-2']" />`, template: `<color-circles :colors="['--color-secondary', '--color-secondary-tint-1', '--color-secondary-tint-2', '--color-secondary-tint-3', '--color-secondary-tint-4']" />`,
components: { components: {
ColorCircles, ColorCircles,
}, },

View File

@@ -212,11 +212,6 @@ export interface IStartRunData {
pinData?: IPinData; pinData?: IPinData;
} }
export interface IRunDataUi {
node?: string;
workflowData: IWorkflowData;
}
export interface ITableData { export interface ITableData {
columns: string[]; columns: string[];
data: GenericValue[][]; data: GenericValue[][];
@@ -863,6 +858,7 @@ export interface IRootState {
oauthCallbackUrls: object; oauthCallbackUrls: object;
n8nMetadata: object; n8nMetadata: object;
workflowExecutionData: IExecutionResponse | null; workflowExecutionData: IExecutionResponse | null;
workflowExecutionPairedItemMappings: {[itemId: string]: Set<string>};
lastSelectedNode: string | null; lastSelectedNode: string | null;
lastSelectedNodeOutputIndex: number | null; lastSelectedNodeOutputIndex: number | null;
nodeViewOffsetPosition: XYPosition; nodeViewOffsetPosition: XYPosition;
@@ -912,6 +908,13 @@ export interface IModalState {
export type IRunDataDisplayMode = 'table' | 'json' | 'binary'; export type IRunDataDisplayMode = 'table' | 'json' | 'binary';
export interface TargetItem {
nodeName: string;
itemIndex: number;
runIndex: number;
outputIndex: number;
}
export interface IUiState { export interface IUiState {
sidebarMenuCollapsed: boolean; sidebarMenuCollapsed: boolean;
modalStack: string[]; modalStack: string[];
@@ -925,11 +928,15 @@ export interface IUiState {
sessionId: string; sessionId: string;
input: { input: {
displayMode: IRunDataDisplayMode; displayMode: IRunDataDisplayMode;
nodeName?: string;
run?: number;
branch?: number;
data: { data: {
isEmpty: boolean; isEmpty: boolean;
} }
}; };
output: { output: {
branch?: number;
displayMode: IRunDataDisplayMode; displayMode: IRunDataDisplayMode;
data: { data: {
isEmpty: boolean; isEmpty: boolean;
@@ -941,6 +948,7 @@ export interface IUiState {
}; };
focusedMappableInput: string; focusedMappableInput: string;
mappingTelemetry: {[key: string]: string | number | boolean}; mappingTelemetry: {[key: string]: string | number | boolean};
hoveringItem: null | TargetItem;
}; };
mainPanelPosition: number; mainPanelPosition: number;
draggable: { draggable: {

View File

@@ -2,7 +2,7 @@
<div @keydown.stop :class="$style.container" v-if="credentialProperties.length"> <div @keydown.stop :class="$style.container" v-if="credentialProperties.length">
<form v-for="parameter in credentialProperties" :key="parameter.name" autocomplete="off"> <form v-for="parameter in credentialProperties" :key="parameter.name" autocomplete="off">
<!-- Why form? to break up inputs, to prevent Chrome autofill --> <!-- Why form? to break up inputs, to prevent Chrome autofill -->
<ParameterInputExpanded <parameter-input-expanded
:parameter="parameter" :parameter="parameter"
:value="credentialData[parameter.name]" :value="credentialData[parameter.name]"
:documentationUrl="documentationUrl" :documentationUrl="documentationUrl"

View File

@@ -14,6 +14,7 @@
:label="$locale.nodeText().inputLabelDisplayName(property, path)" :label="$locale.nodeText().inputLabelDisplayName(property, path)"
:underline="true" :underline="true"
size="small" size="small"
color="text-dark"
/> />
<div v-if="multipleValues === true"> <div v-if="multipleValues === true">
<div <div

View File

@@ -8,7 +8,7 @@
> >
<template slot="content"> <template slot="content">
<div :class="$style.container"> <div :class="$style.container">
<n8n-input-label :label="$locale.baseText('importCurlModal.input.label')"> <n8n-input-label :label="$locale.baseText('importCurlModal.input.label')" color="text-dark">
<n8n-input <n8n-input
:value="curlCommand" :value="curlCommand"
type="textarea" type="textarea"

View File

@@ -14,6 +14,7 @@
:showMappingHint="draggableHintShown" :showMappingHint="draggableHintShown"
:distanceFromActive="currentNodeDepth" :distanceFromActive="currentNodeDepth"
paneType="input" paneType="input"
@itemHover="$emit('itemHover', $event)"
@linkRun="onLinkRun" @linkRun="onLinkRun"
@unlinkRun="onUnlinkRun" @unlinkRun="onUnlinkRun"
@runChange="onRunIndexChange" @runChange="onRunIndexChange"

View File

@@ -5,6 +5,7 @@
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)" :tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
:underline="true" :underline="true"
size="small" size="small"
color="text-dark"
/> />
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type"> <div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">

View File

@@ -13,6 +13,7 @@
:bold="false" :bold="false"
:set="issues = getIssues(credentialTypeDescription.name)" :set="issues = getIssues(credentialTypeDescription.name)"
size="small" size="small"
color="text-dark"
> >
<div v-if="isReadOnly"> <div v-if="isReadOnly">
<n8n-input <n8n-input

View File

@@ -62,6 +62,7 @@
@select="onInputSelect" @select="onInputSelect"
@execute="onNodeExecute" @execute="onNodeExecute"
@tableMounted="onInputTableMounted" @tableMounted="onInputTableMounted"
@itemHover="onInputItemHover"
/> />
</template> </template>
<template #output> <template #output>
@@ -76,6 +77,7 @@
@runChange="onRunOutputIndexChange" @runChange="onRunOutputIndexChange"
@openSettings="openSettings" @openSettings="openSettings"
@tableMounted="onOutputTableMounted" @tableMounted="onOutputTableMounted"
@itemHover="onOutputItemHover"
/> />
</template> </template>
<template #main> <template #main>
@@ -111,7 +113,7 @@ import {
IRunExecutionData, IRunExecutionData,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { IExecutionResponse, INodeUi, IUpdateInformation } from '../Interface'; import { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '../Interface';
import { externalHooks } from '@/components/mixins/externalHooks'; import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@@ -200,7 +202,7 @@ export default mixins(
this.executionWaitingForWebhook this.executionWaitingForWebhook
); );
}, },
activeNode(): INodeUi { activeNode(): INodeUi | null {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;
}, },
inputNodeName(): string | undefined { inputNodeName(): string | undefined {
@@ -394,8 +396,45 @@ export default mixins(
maxInputRun() { maxInputRun() {
this.runInputIndex = -1; this.runInputIndex = -1;
}, },
inputNodeName(nodeName: string | undefined) {
this.$store.commit('ui/setInputNodeName', nodeName);
},
inputRun() {
this.$store.commit('ui/setInputRunIndex', this.inputRun);
},
}, },
methods: { methods: {
onInputItemHover(e: {itemIndex: number, outputIndex: number} | null) {
if (!this.inputNodeName) {
return;
}
if (e === null) {
this.$store.commit('ui/setHoveringItem', null);
return;
}
const item: TargetItem = {
nodeName: this.inputNodeName,
runIndex: this.inputRun,
outputIndex: e.outputIndex,
itemIndex: e.itemIndex,
};
this.$store.commit('ui/setHoveringItem', item);
},
onOutputItemHover(e: {itemIndex: number, outputIndex: number} | null) {
if (e === null || !this.activeNode) {
this.$store.commit('ui/setHoveringItem', null);
return;
}
const item: TargetItem = {
nodeName: this.activeNode.name,
runIndex: this.outputRun,
outputIndex: e.outputIndex,
itemIndex: e.itemIndex,
};
this.$store.commit('ui/setHoveringItem', item);
},
onInputTableMounted(e: { avgRowHeight: number }) { onInputTableMounted(e: { avgRowHeight: number }) {
this.avgInputRowHeight = e.avgRowHeight; this.avgInputRowHeight = e.avgRowHeight;
}, },
@@ -410,13 +449,15 @@ export default mixins(
}, },
onFeatureRequestClick() { onFeatureRequestClick() {
window.open(this.featureRequestUrl, '_blank'); window.open(this.featureRequestUrl, '_blank');
this.$telemetry.track('User clicked ndv link', { if (this.activeNode) {
node_type: this.activeNode.type, this.$telemetry.track('User clicked ndv link', {
workflow_id: this.$store.getters.workflowId, node_type: this.activeNode.type,
session_id: this.sessionId, workflow_id: this.$store.getters.workflowId,
pane: 'main', session_id: this.sessionId,
type: 'i-wish-this-node-would', pane: 'main',
}); type: 'i-wish-this-node-would',
});
}
}, },
onPanelsInit(e: { position: number }) { onPanelsInit(e: { position: number }) {
this.mainPanelPosition = e.position; this.mainPanelPosition = e.position;

View File

@@ -15,6 +15,7 @@
@linkRun="onLinkRun" @linkRun="onLinkRun"
@unlinkRun="onUnlinkRun" @unlinkRun="onUnlinkRun"
@tableMounted="$emit('tableMounted', $event)" @tableMounted="$emit('tableMounted', $event)"
@itemHover="$emit('itemHover', $event)"
ref="runData" ref="runData"
> >
<template v-slot:header> <template v-slot:header>

View File

@@ -21,6 +21,7 @@
:value="value" :value="value"
:displayTitle="displayTitle" :displayTitle="displayTitle"
:expressionDisplayValue="expressionDisplayValue" :expressionDisplayValue="expressionDisplayValue"
:expressionComputedValue="expressionEvaluated"
:isValueExpression="isValueExpression" :isValueExpression="isValueExpression"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:parameterIssues="getIssues" :parameterIssues="getIssues"
@@ -37,7 +38,7 @@
:size="inputSize" :size="inputSize"
:type="getStringInputType" :type="getStringInputType"
:rows="getArgument('rows')" :rows="getArgument('rows')"
:value="activeDrop || forceShowExpression? '': expressionDisplayValue" :value="expressionDisplayValue"
:title="displayTitle" :title="displayTitle"
@keydown.stop @keydown.stop
/> />
@@ -297,6 +298,8 @@ import {
INodeParameters, INodeParameters,
INodePropertyOptions, INodePropertyOptions,
Workflow, Workflow,
INodeProperties,
INodePropertyCollection,
NodeParameterValueType, NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
@@ -316,12 +319,13 @@ import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage'; import { showMessage } from '@/components/mixins/showMessage';
import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression } from './helpers';
import { isResourceLocatorValue } from '@/typeGuards';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { CUSTOM_API_CALL_KEY } from '@/constants'; import { CUSTOM_API_CALL_KEY } from '@/constants';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { hasExpressionMapping, isValueExpression } from './helpers'; import { PropType } from 'vue';
import { isResourceLocatorValue } from '@/typeGuards';
export default mixins( export default mixins(
externalHooks, externalHooks,
@@ -344,21 +348,56 @@ export default mixins(
TextEdit, TextEdit,
ImportParameter, ImportParameter,
}, },
props: [ props: {
'inputSize', isReadOnly: {
'isReadOnly', type: Boolean,
'documentationUrl', },
'parameter', // NodeProperties parameter: {
'path', // string type: Object as PropType<INodeProperties>,
'value', },
'hideIssues', // boolean path: {
'errorHighlight', type: String,
'isForCredential', // boolean },
'eventSource', // string value: {
'activeDrop', type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>,
'droppable', },
'forceShowExpression', hideLabel: {
], type: Boolean,
},
droppable: {
type: Boolean,
},
activeDrop: {
type: Boolean,
},
forceShowExpression: {
type: Boolean,
},
hint: {
type: String as PropType<string | undefined>,
},
inputSize: {
type: String,
},
hideIssues: {
type: Boolean,
},
documentationUrl: {
type: String as PropType<string | undefined>,
},
errorHighlight: {
type: Boolean,
},
isForCredential: {
type: Boolean,
},
eventSource: {
type: String,
},
expressionEvaluated: {
type: String as PropType<string | undefined>,
},
},
data () { data () {
return { return {
codeEditDialogVisible: false, codeEditDialogVisible: false,
@@ -419,12 +458,21 @@ export default mixins(
}, },
computed: { computed: {
...mapGetters('credentials', ['allCredentialTypes']), ...mapGetters('credentials', ['allCredentialTypes']),
expressionDisplayValue(): string {
if (this.activeDrop || this.forceShowExpression) {
return '';
}
const value = isResourceLocatorValue(this.value) ? this.value.value : this.value;
if (typeof value === 'string' && value.startsWith('=')) {
return value.slice(1);
}
return '';
},
isValueExpression(): boolean { isValueExpression(): boolean {
return isValueExpression(this.parameter, this.value); return isValueExpression(this.parameter, this.value);
}, },
areExpressionsDisabled(): boolean {
return this.$store.getters['ui/areExpressionsDisabled'];
},
codeAutocomplete (): string | undefined { codeAutocomplete (): string | undefined {
return this.getArgument('codeAutocomplete') as string | undefined; return this.getArgument('codeAutocomplete') as string | undefined;
}, },
@@ -486,9 +534,9 @@ export default mixins(
let returnValue; let returnValue;
if (this.isValueExpression === false) { if (this.isValueExpression === false) {
returnValue = this.isResourceLocatorParameter ? (this.value ? this.value.value: '') : this.value; returnValue = this.isResourceLocatorParameter ? (isResourceLocatorValue(this.value) ? this.value.value: '') : this.value;
} else { } else {
returnValue = this.expressionValueComputed; returnValue = this.expressionEvaluated;
} }
if (this.parameter.type === 'credentialsSelect') { if (this.parameter.type === 'credentialsSelect') {
@@ -519,39 +567,6 @@ export default mixins(
return returnValue; return returnValue;
}, },
expressionDisplayValue (): string {
const value = this.displayValue;
// address type errors for text input
if (typeof value === 'number' || typeof value === 'boolean') {
return JSON.stringify(value);
}
if (value === null) {
return '';
}
return value;
},
expressionValueComputed (): NodeParameterValue | string[] | null {
if (this.areExpressionsDisabled) {
return this.value;
}
if (this.node === null) {
return null;
}
let computedValue: NodeParameterValue;
try {
computedValue = this.resolveExpression(this.value.value || this.value) as NodeParameterValue;
} catch (error) {
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
}
return computedValue;
},
getStringInputType () { getStringInputType () {
if (this.getArgument('password') === true) { if (this.getArgument('password') === true) {
return 'password'; return 'password';
@@ -562,7 +577,7 @@ export default mixins(
return 'textarea'; return 'textarea';
} }
if (this.parameter.type === 'code') { if (this.parameter.typeOptions && this.parameter.typeOptions.editor === 'code') {
return 'textarea'; return 'textarea';
} }
@@ -587,13 +602,14 @@ export default mixins(
} else if ( } else if (
['options', 'multiOptions'].includes(this.parameter.type) && ['options', 'multiOptions'].includes(this.parameter.type) &&
this.remoteParameterOptionsLoading === false && this.remoteParameterOptionsLoading === false &&
this.remoteParameterOptionsLoadingIssues === null this.remoteParameterOptionsLoadingIssues === null &&
this.parameterOptions
) { ) {
// Check if the value resolves to a valid option // Check if the value resolves to a valid option
// Currently it only displays an error in the node itself in // Currently it only displays an error in the node itself in
// case the value is not valid. The workflow can still be executed // case the value is not valid. The workflow can still be executed
// and the error is not displayed on the node in the workflow // and the error is not displayed on the node in the workflow
const validOptions = this.parameterOptions!.map((options: INodePropertyOptions) => options.value); const validOptions = this.parameterOptions.map((options) => (options as INodePropertyOptions).value);
const checkValues: string[] = []; const checkValues: string[] = [];
@@ -640,7 +656,7 @@ export default mixins(
editorType (): string { editorType (): string {
return this.getArgument('editor') as string; return this.getArgument('editor') as string;
}, },
parameterOptions (): INodePropertyOptions[] { parameterOptions (): Array<INodePropertyOptions | INodeProperties | INodePropertyCollection> | undefined {
if (this.hasRemoteMethod === false) { if (this.hasRemoteMethod === false) {
// Options are already given // Options are already given
return this.parameter.options; return this.parameter.options;
@@ -828,10 +844,6 @@ export default mixins(
this.valueChanged(val); this.valueChanged(val);
}, },
openExpressionEdit() { openExpressionEdit() {
if (this.areExpressionsDisabled) {
return;
}
if (this.isValueExpression) { if (this.isValueExpression) {
this.expressionEditDialogVisible = true; this.expressionEditDialogVisible = true;
this.trackExpressionEditOpen(); this.trackExpressionEditOpen();
@@ -896,7 +908,7 @@ export default mixins(
this.$emit('textInput', parameterData); this.$emit('textInput', parameterData);
}, },
valueChanged (value: string[] | string | number | boolean | Date | {} | null) { valueChanged (value: NodeParameterValueType | {} | Date) {
if (this.parameter.name === 'nodeCredentialType') { if (this.parameter.name === 'nodeCredentialType') {
this.activeCredentialType = value as string; this.activeCredentialType = value as string;
} }
@@ -905,7 +917,7 @@ export default mixins(
value = value.toISOString(); value = value.toISOString();
} }
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value.toString().charAt(0) !== '#') { if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value !== undefined && value.toString().charAt(0) !== '#') {
const newValue = this.rgbaToHex(value as string); const newValue = this.rgbaToHex(value as string);
if (newValue !== null) { if (newValue !== null) {
this.tempValue = newValue; this.tempValue = newValue;
@@ -959,14 +971,14 @@ export default mixins(
this.trackExpressionEditOpen(); this.trackExpressionEditOpen();
}, 375); }, 375);
} else if (command === 'removeExpression') { } else if (command === 'removeExpression') {
let value = this.expressionValueComputed; let value: NodeParameterValueType = this.expressionEvaluated;
if (this.parameter.type === 'multiOptions' && typeof value === 'string') { if (this.parameter.type === 'multiOptions' && typeof value === 'string') {
value = (value || '').split(',') value = (value || '').split(',')
.filter((value) => (this.parameterOptions || []).find((option) => option.value === value)); .filter((value) => (this.parameterOptions || []).find((option) => (option as INodePropertyOptions).value === value));
} }
if (this.isResourceLocatorParameter) { if (this.isResourceLocatorParameter && isResourceLocatorValue(this.value)) {
this.valueChanged({ __rl: true, value, mode: this.value.mode }); this.valueChanged({ __rl: true, value, mode: this.value.mode });
} else { } else {
this.valueChanged(typeof value !== 'undefined' ? value : null); this.valueChanged(typeof value !== 'undefined' ? value : null);

View File

@@ -18,19 +18,18 @@
/> />
</template> </template>
<template> <template>
<parameter-input <parameter-input-wrapper
ref="param" ref="param"
inputSize="large" inputSize="large"
:parameter="parameter" :parameter="parameter"
:value="value" :value="value"
:path="parameter.name" :path="parameter.name"
:hideIssues="true" :hideIssues="true"
:displayOptions="true"
:documentationUrl="documentationUrl" :documentationUrl="documentationUrl"
:errorHighlight="showRequiredErrors" :errorHighlight="showRequiredErrors"
:isForCredential="true" :isForCredential="true"
:eventSource="eventSource" :eventSource="eventSource"
:isValueExpression="isValueExpression" :hint="!showRequiredErrors? hint: ''"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@textInput="valueChanged" @textInput="valueChanged"
@@ -44,30 +43,27 @@
</n8n-link> </n8n-link>
</n8n-text> </n8n-text>
</div> </div>
<input-hint :class="$style.hint" :hint="$locale.credText().hint(parameter)" />
</template> </template>
</n8n-input-label> </n8n-input-label>
</template> </template>
<script lang="ts"> <script lang="ts">
import { IUpdateInformation } from '@/Interface'; import { IUpdateInformation } from '@/Interface';
import ParameterInput from './ParameterInput.vue';
import ParameterOptions from './ParameterOptions.vue'; import ParameterOptions from './ParameterOptions.vue';
import InputHint from './ParameterInputHint.vue'; import Vue, { PropType } from 'vue';
import Vue from 'vue'; import ParameterInputWrapper from './ParameterInputWrapper.vue';
import { isValueExpression } from './helpers'; import { isValueExpression } from './helpers';
import { INodeParameterResourceLocator, INodeProperties } from 'n8n-workflow'; import { INodeParameterResourceLocator, INodeProperties } from 'n8n-workflow';
export default Vue.extend({ export default Vue.extend({
name: 'ParameterInputExpanded', name: 'parameter-input-expanded',
components: { components: {
ParameterInput,
InputHint,
ParameterOptions, ParameterOptions,
ParameterInputWrapper,
}, },
props: { props: {
parameter: { parameter: {
type: Object as () => INodeProperties, type: Object as PropType<INodeProperties>,
}, },
value: { value: {
}, },
@@ -106,6 +102,13 @@ export default Vue.extend({
return false; return false;
}, },
hint(): string | null {
if (this.isValueExpression) {
return null;
}
return this.$locale.credText().hint(this.parameter);
},
isValueExpression (): boolean { isValueExpression (): boolean {
return isValueExpression(this.parameter, this.value as string | INodeParameterResourceLocator); return isValueExpression(this.parameter, this.value as string | INodeParameterResourceLocator);
}, },

View File

@@ -6,6 +6,7 @@
:showOptions="menuExpanded || focused || forceShowExpression" :showOptions="menuExpanded || focused || forceShowExpression"
:bold="false" :bold="false"
size="small" size="small"
color="text-dark"
> >
<template #options> <template #options>
<parameter-options <parameter-options
@@ -34,16 +35,16 @@
:buttons="dataMappingTooltipButtons" :buttons="dataMappingTooltipButtons"
> >
<span slot="content" v-html="$locale.baseText(`dataMapping.${displayMode}Hint`, { interpolate: { name: parameter.displayName } })" /> <span slot="content" v-html="$locale.baseText(`dataMapping.${displayMode}Hint`, { interpolate: { name: parameter.displayName } })" />
<parameter-input <parameter-input-wrapper
ref="param" ref="param"
:parameter="parameter" :parameter="parameter"
:value="value" :value="value"
:displayOptions="displayOptions"
:path="path" :path="path"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:droppable="droppable" :droppable="droppable"
:activeDrop="activeDrop" :activeDrop="activeDrop"
:forceShowExpression="forceShowExpression" :forceShowExpression="forceShowExpression"
:hint="hint"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@@ -53,7 +54,6 @@
</n8n-tooltip> </n8n-tooltip>
</template> </template>
</draggable-target> </draggable-target>
<input-hint :class="$style.hint" :hint="$locale.nodeText().hint(parameter, path)" />
</template> </template>
</n8n-input-label> </n8n-input-label>
</template> </template>
@@ -68,7 +68,6 @@ import {
IUpdateInformation, IUpdateInformation,
} from '@/Interface'; } from '@/Interface';
import ParameterInput from '@/components/ParameterInput.vue';
import InputHint from './ParameterInputHint.vue'; import InputHint from './ParameterInputHint.vue';
import ParameterOptions from './ParameterOptions.vue'; import ParameterOptions from './ParameterOptions.vue';
import DraggableTarget from '@/components/DraggableTarget.vue'; import DraggableTarget from '@/components/DraggableTarget.vue';
@@ -76,6 +75,7 @@ import mixins from 'vue-typed-mixins';
import { showMessage } from './mixins/showMessage'; import { showMessage } from './mixins/showMessage';
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants'; import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
import { hasExpressionMapping } from './helpers'; import { hasExpressionMapping } from './helpers';
import ParameterInputWrapper from './ParameterInputWrapper.vue';
import { hasOnlyListMode } from './ResourceLocator/helpers'; import { hasOnlyListMode } from './ResourceLocator/helpers';
import { INodePropertyMode } from 'n8n-workflow'; import { INodePropertyMode } from 'n8n-workflow';
import { isResourceLocatorValue } from '@/typeGuards'; import { isResourceLocatorValue } from '@/typeGuards';
@@ -87,10 +87,10 @@ export default mixins(
.extend({ .extend({
name: 'parameter-input-full', name: 'parameter-input-full',
components: { components: {
ParameterInput,
InputHint, InputHint,
ParameterOptions, ParameterOptions,
DraggableTarget, DraggableTarget,
ParameterInputWrapper,
}, },
data() { data() {
return { return {
@@ -125,6 +125,9 @@ export default mixins(
node (): INodeUi | null { node (): INodeUi | null {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;
}, },
hint (): string | null {
return this.$locale.nodeText().hint(this.parameter, this.path);
},
isResourceLocator (): boolean { isResourceLocator (): boolean {
return this.parameter.type === 'resourceLocator'; return this.parameter.type === 'resourceLocator';
}, },
@@ -256,9 +259,3 @@ export default mixins(
}, },
}); });
</script> </script>
<style lang="scss" module>
.hint {
margin-top: var(--spacing-4xs);
}
</style>

View File

@@ -1,9 +1,7 @@
<template> <template>
<div> <n8n-text size="small" color="text-base" tag="div" v-if="hint">
<n8n-text size="xsmall" color="text-base" v-if="hint"> <div ref="hint" :class="{[$style.hint]: true, [$style.highlight]: highlight}" v-html="hint"></div>
<div ref="hint" v-html="hint"></div> </n8n-text>
</n8n-text>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -11,7 +9,14 @@ import Vue from "vue";
export default Vue.extend({ export default Vue.extend({
name: 'InputHint', name: 'InputHint',
props: ['hint'], props: {
hint: {
type: String,
},
highlight: {
type: Boolean,
},
},
mounted(){ mounted(){
if(this.$refs.hint){ if(this.$refs.hint){
(this.$refs.hint as Element).querySelectorAll('a').forEach(a => a.target = "_blank"); (this.$refs.hint as Element).querySelectorAll('a').forEach(a => a.target = "_blank");
@@ -20,3 +25,17 @@ export default Vue.extend({
}); });
</script> </script>
<style lang="scss" module>
.hint {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.highlight {
color: var(--color-secondary);
}
</style>

View File

@@ -45,6 +45,7 @@
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)" :tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
size="small" size="small"
:underline="true" :underline="true"
color="text-dark"
/> />
<collection-parameter <collection-parameter
v-if="parameter.type === 'collection'" v-if="parameter.type === 'collection'"

View File

@@ -0,0 +1,210 @@
<template>
<div>
<parameter-input
ref="param"
:inputSize="inputSize"
:parameter="parameter"
:value="value"
:path="path"
:isReadOnly="isReadOnly"
:droppable="droppable"
:activeDrop="activeDrop"
:forceShowExpression="forceShowExpression"
:hideIssues="hideIssues"
:documentationUrl="documentationUrl"
:errorHighlight="errorHighlight"
:isForCredential="isForCredential"
:eventSource="eventSource"
:expressionEvaluated="expressionValueComputed"
@focus="onFocus"
@blur="onBlur"
@drop="onDrop"
@textInput="onTextInput"
@valueChanged="onValueChanged" />
<input-hint v-if="expressionOutput || parameterHint" :class="$style.hint" :highlight="!!(expressionOutput && targetItem)" :hint="expressionOutput || parameterHint" />
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import ParameterInput from '@/components/ParameterInput.vue';
import InputHint from './ParameterInputHint.vue';
import mixins from 'vue-typed-mixins';
import { showMessage } from './mixins/showMessage';
import { INodeProperties, INodePropertyMode, IRunData, isResourceLocatorValue, NodeParameterValue, NodeParameterValueType } from 'n8n-workflow';
import { INodeUi, IUiState, IUpdateInformation, TargetItem } from '@/Interface';
import { workflowHelpers } from './mixins/workflowHelpers';
import { isValueExpression } from './helpers';
export default mixins(
showMessage,
workflowHelpers,
)
.extend({
name: 'parameter-input-wrapper',
components: {
ParameterInput,
InputHint,
},
mounted() {
this.$on('optionSelected', this.optionSelected);
},
props: {
isReadOnly: {
type: Boolean,
},
parameter: {
type: Object as PropType<INodeProperties>,
},
path: {
type: String,
},
value: {
type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>,
},
hideLabel: {
type: Boolean,
},
droppable: {
type: Boolean,
},
activeDrop: {
type: Boolean,
},
forceShowExpression: {
type: Boolean,
},
hint: {
type: String,
required: false,
},
inputSize: {
type: String,
},
hideIssues: {
type: Boolean,
},
documentationUrl: {
type: String as PropType<string | undefined>,
},
errorHighlight: {
type: Boolean,
},
isForCredential: {
type: Boolean,
},
eventSource: {
type: String,
},
},
computed: {
isValueExpression () {
return isValueExpression(this.parameter, this.value);
},
activeNode(): INodeUi | null {
return this.$store.getters.activeNode;
},
selectedRLMode(): INodePropertyMode | undefined {
if (typeof this.value !== 'object' ||this.parameter.type !== 'resourceLocator' || !isResourceLocatorValue(this.value)) {
return undefined;
}
const mode = this.value.mode;
if (mode) {
return this.parameter.modes?.find((m: INodePropertyMode) => m.name === mode);
}
return undefined;
},
parameterHint(): string | undefined {
if (this.isValueExpression) {
return undefined;
}
if (this.selectedRLMode && this.selectedRLMode.hint) {
return this.selectedRLMode.hint;
}
return this.hint;
},
targetItem(): TargetItem | null {
return this.$store.getters['ui/hoveringItem'];
},
expressionValueComputed (): string | null {
const inputNodeName: string | undefined = this.$store.getters['ui/ndvInputNodeName'];
const value = isResourceLocatorValue(this.value)? this.value.value: this.value;
if (this.activeNode === null || !this.isValueExpression || typeof value !== 'string') {
return null;
}
const inputRunIndex: number | undefined = this.$store.getters['ui/ndvInputRunIndex'];
const inputBranchIndex: number | undefined = this.$store.getters['ui/ndvInputBranchIndex'];
let computedValue: NodeParameterValue;
try {
const targetItem = this.targetItem ?? undefined;
computedValue = this.resolveExpression(value, undefined, {targetItem, inputNodeName, inputRunIndex, inputBranchIndex});
if (computedValue === null) {
return null;
}
if (typeof computedValue === 'string' && computedValue.trim().length === 0) {
computedValue = this.$locale.baseText('parameterInput.emptyString');
}
} catch (error) {
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
}
return typeof computedValue === 'string' ? computedValue : JSON.stringify(computedValue);
},
expressionOutput(): string | null {
if (this.isValueExpression && this.expressionValueComputed) {
const inputData = this.$store.getters['ui/ndvInputData'];
if (!inputData || (inputData && inputData.length <= 1)) {
return this.expressionValueComputed;
}
return this.$locale.baseText(`parameterInput.expressionResult`, {
interpolate: {
result: this.expressionValueComputed,
},
});
}
return null;
},
},
methods: {
onFocus() {
this.$emit('focus');
},
onBlur() {
this.$emit('blur');
},
onDrop(data: string) {
this.$emit('drop', data);
},
optionSelected(command: string) {
if (this.$refs.param) {
(this.$refs.param as Vue).$emit('optionSelected', command);
}
},
onValueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
onTextInput(parameterData: IUpdateInformation) {
this.$emit('textInput', parameterData);
},
},
});
</script>
<style lang="scss" module>
.hint {
margin-top: var(--spacing-4xs);
}
.hovering {
color: var(--color-secondary);
}
</style>

View File

@@ -82,7 +82,7 @@
v-if="isValueExpression || droppable || forceShowExpression" v-if="isValueExpression || droppable || forceShowExpression"
type="text" type="text"
:size="inputSize" :size="inputSize"
:value="activeDrop || forceShowExpression ? '' : expressionDisplayValue" :value="expressionDisplayValue"
:title="displayTitle" :title="displayTitle"
@keydown.stop @keydown.stop
ref="input" ref="input"
@@ -137,7 +137,6 @@
</div> </div>
</div> </div>
</resource-locator-dropdown> </resource-locator-dropdown>
<parameter-input-hint v-if="infoText" class="mt-4xs" :hint="infoText" />
</div> </div>
</template> </template>
@@ -163,7 +162,6 @@ import {
import DraggableTarget from '@/components/DraggableTarget.vue'; import DraggableTarget from '@/components/DraggableTarget.vue';
import ExpressionEdit from '@/components/ExpressionEdit.vue'; import ExpressionEdit from '@/components/ExpressionEdit.vue';
import ParameterIssues from '@/components/ParameterIssues.vue'; import ParameterIssues from '@/components/ParameterIssues.vue';
import ParameterInputHint from '@/components/ParameterInputHint.vue';
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue'; import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
import Vue, { PropType } from 'vue'; import Vue, { PropType } from 'vue';
import { INodeUi, IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface'; import { INodeUi, IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface';
@@ -172,7 +170,6 @@ import stringify from 'fast-json-stable-stringify';
import { workflowHelpers } from '../mixins/workflowHelpers'; import { workflowHelpers } from '../mixins/workflowHelpers';
import { nodeHelpers } from '../mixins/nodeHelpers'; import { nodeHelpers } from '../mixins/nodeHelpers';
import { getAppNameFromNodeName } from '../helpers'; import { getAppNameFromNodeName } from '../helpers';
import { type } from 'os';
import { isResourceLocatorValue } from '@/typeGuards'; import { isResourceLocatorValue } from '@/typeGuards';
interface IResourceLocatorQuery { interface IResourceLocatorQuery {
@@ -188,7 +185,6 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
DraggableTarget, DraggableTarget,
ExpressionEdit, ExpressionEdit,
ParameterIssues, ParameterIssues,
ParameterInputHint,
ResourceLocatorDropdown, ResourceLocatorDropdown,
}, },
props: { props: {
@@ -216,7 +212,7 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
type: String, type: String,
default: '', default: '',
}, },
expressionDisplayValue: { expressionComputedValue: {
type: String, type: String,
default: '', default: '',
}, },
@@ -224,6 +220,9 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
expressionDisplayValue: {
type: String,
},
forceShowExpression: { forceShowExpression: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -298,9 +297,6 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
return defaults[this.selectedMode] || ''; return defaults[this.selectedMode] || '';
}, },
infoText(): string {
return this.currentMode.hint ? this.currentMode.hint : '';
},
currentMode(): INodePropertyMode { currentMode(): INodePropertyMode {
return this.findModeByName(this.selectedMode) || ({} as INodePropertyMode); return this.findModeByName(this.selectedMode) || ({} as INodePropertyMode);
}, },
@@ -327,8 +323,8 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
} }
if (this.selectedMode === 'url') { if (this.selectedMode === 'url') {
if (this.isValueExpression && typeof this.expressionDisplayValue === 'string' && this.expressionDisplayValue.startsWith('http')) { if (this.isValueExpression && typeof this.expressionComputedValue === 'string' && this.expressionComputedValue.startsWith('http')) {
return this.expressionDisplayValue; return this.expressionComputedValue;
} }
if (typeof this.valueToDisplay === 'string' && this.valueToDisplay.startsWith('http')) { if (typeof this.valueToDisplay === 'string' && this.valueToDisplay.startsWith('http')) {
@@ -337,7 +333,7 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
} }
if (this.currentMode.url) { if (this.currentMode.url) {
const value = this.isValueExpression? this.expressionDisplayValue : this.valueToDisplay; const value = this.isValueExpression? this.expressionComputedValue : this.valueToDisplay;
if (typeof value === 'string') { if (typeof value === 'string') {
const expression = this.currentMode.url.replace(/\{\{\$value\}\}/g, value); const expression = this.currentMode.url.replace(/\{\{\$value\}\}/g, value);
const resolved = this.resolveExpression(expression); const resolved = this.resolveExpression(expression);

View File

@@ -220,8 +220,11 @@
:distanceFromActive="distanceFromActive" :distanceFromActive="distanceFromActive"
:showMappingHint="showMappingHint" :showMappingHint="showMappingHint"
:runIndex="runIndex" :runIndex="runIndex"
:pageOffset="currentPageOffset"
:totalRuns="maxRunIndex" :totalRuns="maxRunIndex"
:hasDefaultHoverState="paneType === 'input'"
@mounted="$emit('tableMounted', $event)" @mounted="$emit('tableMounted', $event)"
@activeRowChanged="onItemHover"
/> />
<run-data-json <run-data-json
@@ -419,7 +422,7 @@ export default mixins(
type: String, type: String,
}, },
overrideOutputs: { overrideOutputs: {
type: Array, type: Array as PropType<number[]>,
}, },
mappingEnabled: { mappingEnabled: {
type: Boolean, type: Boolean,
@@ -463,6 +466,10 @@ export default mixins(
this.showPinDataDiscoveryTooltip(this.jsonData); this.showPinDataDiscoveryTooltip(this.jsonData);
} }
} }
this.$store.commit('ui/setNDVBranchIndex', {
pane: this.paneType,
branchIndex: this.currentOutputIndex,
});
}, },
destroyed() { destroyed() {
this.hidePinDataDiscoveryTooltip(); this.hidePinDataDiscoveryTooltip();
@@ -561,6 +568,9 @@ export default mixins(
return 0; return 0;
}, },
currentPageOffset(): number {
return this.pageSize * (this.currentPage - 1);
},
maxRunIndex (): number { maxRunIndex (): number {
if (this.node === null) { if (this.node === null) {
return 0; return 0;
@@ -662,6 +672,17 @@ export default mixins(
}, },
}, },
methods: { methods: {
onItemHover(itemIndex: number | null) {
if (itemIndex === null) {
this.$emit('itemHover', null);
return;
}
this.$emit('itemHover', {
outputIndex: this.currentOutputIndex,
itemIndex,
});
},
onClickDataPinningDocsLink() { onClickDataPinningDocsLink() {
this.$telemetry.track('User clicked ndv link', { this.$telemetry.track('User clicked ndv link', {
workflow_id: this.$store.getters.workflowId, workflow_id: this.$store.getters.workflowId,
@@ -1094,6 +1115,12 @@ export default mixins(
this.onDisplayModeChange('table'); this.onDisplayModeChange('table');
} }
}, },
currentOutputIndex(branchIndex: number) {
this.$store.commit('ui/setNDVBranchIndex', {
pane: this.paneType,
branchIndex,
});
},
}, },
}); });
</script> </script>

View File

@@ -5,8 +5,13 @@
<th :class="$style.emptyCell"></th> <th :class="$style.emptyCell"></th>
<th :class="$style.tableRightMargin"></th> <th :class="$style.tableRightMargin"></th>
</tr> </tr>
<tr v-for="(row, index1) in tableData.data" :key="index1"> <tr v-for="(row, index1) in tableData.data" :key="index1" :class="{[$style.hoveringRow]: isHoveringRow(index1)}">
<td> <td
:data-row="index1"
:data-col="0"
@mouseenter="onMouseEnterCell"
@mouseleave="onMouseLeaveCell"
>
<n8n-text>{{ $locale.baseText('runData.emptyItemHint') }}</n8n-text> <n8n-text>{{ $locale.baseText('runData.emptyItemHint') }}</n8n-text>
</td> </td>
<td :class="$style.tableRightMargin"></td> <td :class="$style.tableRightMargin"></td>
@@ -88,10 +93,11 @@
</div> </div>
</template> </template>
<template> <template>
<tr v-for="(row, index1) in tableData.data" :key="index1"> <tr v-for="(row, index1) in tableData.data" :key="index1" :class="{[$style.hoveringRow]: isHoveringRow(index1)}">
<td <td
v-for="(data, index2) in row" v-for="(data, index2) in row"
:key="index2" :key="index2"
:data-row="index1"
:data-col="index2" :data-col="index2"
@mouseenter="onMouseEnterCell" @mouseenter="onMouseEnterCell"
@mouseleave="onMouseLeaveCell" @mouseleave="onMouseLeaveCell"
@@ -136,9 +142,10 @@
<script lang="ts"> <script lang="ts">
/* eslint-disable prefer-spread */ /* eslint-disable prefer-spread */
import { INodeUi, IRootState, ITableData, IUiState } from '@/Interface';
import { getPairedItemId } from '@/pairedItemUtils';
import Vue, { PropType } from 'vue'; import Vue, { PropType } from 'vue';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { INodeUi, ITableData } from '@/Interface';
import { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow'; import { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow';
import Draggable from './Draggable.vue'; import Draggable from './Draggable.vue';
import { shorten } from './helpers'; import { shorten } from './helpers';
@@ -163,9 +170,18 @@ export default mixins(externalHooks).extend({
runIndex: { runIndex: {
type: Number, type: Number,
}, },
outputIndex: {
type: Number,
},
totalRuns: { totalRuns: {
type: Number, type: Number,
}, },
pageOffset: {
type: Number,
},
hasDefaultHoverState: {
type: Boolean,
},
}, },
data() { data() {
return { return {
@@ -174,6 +190,7 @@ export default mixins(externalHooks).extend({
draggingPath: null as null | string, draggingPath: null as null | string,
hoveringPath: null as null | string, hoveringPath: null as null | string,
mappingHintVisible: false, mappingHintVisible: false,
activeRow: null as number | null,
}; };
}, },
mounted() { mounted() {
@@ -187,12 +204,35 @@ export default mixins(externalHooks).extend({
} }
}, },
computed: { computed: {
hoveringItem(): IUiState['ndv']['hoveringItem'] {
return this.$store.getters['ui/hoveringItem'];
},
pairedItemMappings(): IRootState['workflowExecutionPairedItemMappings'] {
return this.$store.getters['workflowExecutionPairedItemMappings'];
},
tableData(): ITableData { tableData(): ITableData {
return this.convertToTable(this.inputData); return this.convertToTable(this.inputData);
}, },
}, },
methods: { methods: {
shorten, shorten,
isHoveringRow(row: number): boolean {
if (row === this.activeRow) {
return true;
}
const itemIndex = this.pageOffset + row;
if (itemIndex === 0 && !this.hoveringItem && this.hasDefaultHoverState && this.distanceFromActive === 1) {
return true;
}
const itemNodeId = getPairedItemId(this.node.name, this.runIndex || 0, this.outputIndex || 0, itemIndex);
if (!this.hoveringItem || !this.pairedItemMappings[itemNodeId]) {
return false;
}
const hoveringItemId = getPairedItemId(this.hoveringItem.nodeName, this.hoveringItem.runIndex, this.hoveringItem.outputIndex, this.hoveringItem.itemIndex);
return this.pairedItemMappings[itemNodeId].has(hoveringItemId);
},
onMouseEnterCell(e: MouseEvent) { onMouseEnterCell(e: MouseEvent) {
const target = e.target; const target = e.target;
if (target && this.mappingEnabled) { if (target && this.mappingEnabled) {
@@ -201,9 +241,19 @@ export default mixins(externalHooks).extend({
this.activeColumn = parseInt(col, 10); this.activeColumn = parseInt(col, 10);
} }
} }
if (target) {
const row = (target as HTMLElement).dataset.row;
if (row && !isNaN(parseInt(row, 10))) {
this.activeRow = parseInt(row, 10);
this.$emit('activeRowChanged', this.pageOffset + this.activeRow);
}
}
}, },
onMouseLeaveCell() { onMouseLeaveCell() {
this.activeColumn = -1; this.activeColumn = -1;
this.activeRow = null;
this.$emit('activeRowChanged', null);
}, },
onMouseEnterKey(path: string[], colIndex: number) { onMouseEnterKey(path: string[], colIndex: number) {
this.hoveringPath = this.getCellExpression(path, colIndex); this.hoveringPath = this.getCellExpression(path, colIndex);
@@ -438,6 +488,7 @@ export default mixins(externalHooks).extend({
position: sticky; position: sticky;
top: 0; top: 0;
color: var(--color-text-dark); color: var(--color-text-dark);
z-index: 1;
} }
td { td {
@@ -449,6 +500,27 @@ export default mixins(externalHooks).extend({
white-space: pre-wrap; white-space: pre-wrap;
} }
td:first-child, td:nth-last-child(2) {
position: relative;
z-index: 0;
&:after { // add border without shifting content
content: '';
position: absolute;
height: 100%;
width: 2px;
top: 0;
}
}
td:nth-last-child(2):after {
right: -1px;
}
td:first-child:after {
left: -1px;
}
th:last-child, th:last-child,
td:last-child { td:last-child {
border-right: var(--border-base); border-right: var(--border-base);
@@ -565,9 +637,16 @@ export default mixins(externalHooks).extend({
.tableRightMargin { .tableRightMargin {
// becomes necessary with large tables // becomes necessary with large tables
background-color: var(--color-background-base) !important;
width: var(--spacing-s); width: var(--spacing-s);
border-right: none !important; border-right: none !important;
border-top: none !important; border-top: none !important;
border-bottom: none !important; border-bottom: none !important;
} }
.hoveringRow {
td:first-child:after, td:nth-last-child(2):after {
background-color: var(--color-secondary);
}
}
</style> </style>

View File

@@ -43,6 +43,7 @@ import {
XYPosition, XYPosition,
ITag, ITag,
IUpdateInformation, IUpdateInformation,
TargetItem,
} from '../../Interface'; } from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks'; import { externalHooks } from '@/components/mixins/externalHooks';
@@ -54,6 +55,7 @@ import { isEqual } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { getSourceItems } from '@/pairedItemUtils';
let cachedWorkflowKey: string | null = ''; let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null; let cachedWorkflow: Workflow | null = null;
@@ -98,6 +100,7 @@ export const workflowHelpers = mixins(
if (!workflowRunData[parentNodeName] || if (!workflowRunData[parentNodeName] ||
workflowRunData[parentNodeName].length <= runIndex || workflowRunData[parentNodeName].length <= runIndex ||
!workflowRunData[parentNodeName][runIndex] ||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') || !workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
workflowRunData[parentNodeName][runIndex].data === undefined || workflowRunData[parentNodeName][runIndex].data === undefined ||
!workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName) !workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName)
@@ -526,24 +529,47 @@ export const workflowHelpers = mixins(
}, },
resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) { resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], opts: {targetItem?: TargetItem, inputNodeName?: string, inputRunIndex?: number, inputBranchIndex?: number} = {}): IDataObject | null {
const itemIndex = 0; let itemIndex = opts?.targetItem?.itemIndex || 0;
const inputName = 'main'; const inputName = 'main';
const activeNode = this.$store.getters.activeNode; const activeNode = this.$store.getters.activeNode;
const workflow = this.getCurrentWorkflow(); const workflow = this.getCurrentWorkflow();
const parentNode = workflow.getParentNodes(activeNode.name, inputName, 1); const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null;
let parentNode = workflow.getParentNodes(activeNode.name, inputName, 1);
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null; const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null; if (opts?.inputNodeName && !parentNode.includes(opts.inputNodeName)) {
let runIndexParent = 0; return null;
if (workflowRunData !== null && parentNode.length) { }
const firstParentWithWorkflowRunData = parentNode.find((parentNodeName) => workflowRunData[parentNodeName]);
if (firstParentWithWorkflowRunData) { let runIndexParent = opts?.inputRunIndex ?? 0;
runIndexParent = workflowRunData[firstParentWithWorkflowRunData].length - 1; const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]);
if (opts.targetItem && opts?.targetItem?.nodeName === activeNode.name && executionData) {
const sourceItems = getSourceItems(executionData, opts.targetItem);
if (!sourceItems.length) {
return null;
}
parentNode = [sourceItems[0].nodeName];
runIndexParent = sourceItems[0].runIndex;
itemIndex = sourceItems[0].itemIndex;
if (nodeConnection) {
nodeConnection.sourceIndex = sourceItems[0].outputIndex;
}
} else {
parentNode = opts.inputNodeName ? [opts.inputNodeName] : parentNode;
if (nodeConnection) {
nodeConnection.sourceIndex = opts.inputBranchIndex ?? nodeConnection.sourceIndex;
}
if (opts?.inputRunIndex === undefined && workflowRunData !== null && parentNode.length) {
const firstParentWithWorkflowRunData = parentNode.find((parentNodeName) => workflowRunData[parentNodeName]);
if (firstParentWithWorkflowRunData) {
runIndexParent = workflowRunData[firstParentWithWorkflowRunData].length - 1;
}
} }
} }
const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]);
let connectionInputData = this.connectionInputData(parentNode, activeNode.name, inputName, runIndexParent, nodeConnection); let connectionInputData = this.connectionInputData(parentNode, activeNode.name, inputName, runIndexParent, nodeConnection);
let runExecutionData: IRunExecutionData; let runExecutionData: IRunExecutionData;
@@ -601,8 +627,8 @@ export const workflowHelpers = mixins(
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
}; };
let runIndexCurrent = 0; let runIndexCurrent = opts?.targetItem?.runIndex ?? 0;
if (workflowRunData !== null && workflowRunData[activeNode.name]) { if (opts?.targetItem === undefined && workflowRunData !== null && workflowRunData[activeNode.name]) {
runIndexCurrent = workflowRunData[activeNode.name].length -1; runIndexCurrent = workflowRunData[activeNode.name].length -1;
} }
const executeData = this.executeData(parentNode, activeNode.name, inputName, runIndexCurrent); const executeData = this.executeData(parentNode, activeNode.name, inputName, runIndexCurrent);
@@ -610,12 +636,15 @@ export const workflowHelpers = mixins(
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndexCurrent, itemIndex, activeNode.name, connectionInputData, 'manual', this.$store.getters.timezone, additionalKeys, executeData, false) as IDataObject; return workflow.expression.getParameterValue(parameter, runExecutionData, runIndexCurrent, itemIndex, activeNode.name, connectionInputData, 'manual', this.$store.getters.timezone, additionalKeys, executeData, false) as IDataObject;
}, },
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) { resolveExpression(expression: string, siblingParameters: INodeParameters = {}, opts: {targetItem?: TargetItem, inputNodeName?: string, inputRunIndex?: number, inputBranchIndex?: number, c?: number} = {}) {
const parameters = { const parameters = {
'__xxxxxxx__': expression, '__xxxxxxx__': expression,
...siblingParameters, ...siblingParameters,
}; };
const returnData = this.resolveParameter(parameters) as IDataObject; const returnData: IDataObject | null = this.resolveParameter(parameters, opts);
if (!returnData) {
return null;
}
if (typeof returnData['__xxxxxxx__'] === 'object') { if (typeof returnData['__xxxxxxx__'] === 'object') {
const workflow = this.getCurrentWorkflow(); const workflow = this.getCurrentWorkflow();

View File

@@ -28,6 +28,7 @@ import {
import Vue from 'vue'; import Vue from 'vue';
import { ActionContext, Module } from 'vuex'; import { ActionContext, Module } from 'vuex';
import { import {
IExecutionResponse,
IFakeDoor, IFakeDoor,
IFakeDoorLocation, IFakeDoorLocation,
IRootState, IRootState,
@@ -117,12 +118,16 @@ const module: Module<IUiState, IRootState> = {
sessionId: '', sessionId: '',
input: { input: {
displayMode: 'table', displayMode: 'table',
nodeName: undefined,
run: undefined,
branch: undefined,
data: { data: {
isEmpty: true, isEmpty: true,
}, },
}, },
output: { output: {
displayMode: 'table', displayMode: 'table',
branch: undefined,
data: { data: {
isEmpty: true, isEmpty: true,
}, },
@@ -133,6 +138,7 @@ const module: Module<IUiState, IRootState> = {
}, },
focusedMappableInput: '', focusedMappableInput: '',
mappingTelemetry: {}, mappingTelemetry: {},
hoveringItem: null,
}, },
mainPanelPosition: 0.5, mainPanelPosition: 0.5,
draggable: { draggable: {
@@ -174,8 +180,17 @@ const module: Module<IUiState, IRootState> = {
], ],
}, },
getters: { getters: {
areExpressionsDisabled(state: IUiState) { ndvInputData: (state: IUiState, getters, rootState: IRootState, rootGetters) => {
return state.currentView === VIEWS.DEMO; const executionData = rootGetters.getWorkflowExecution as IExecutionResponse | null;
const inputNodeName: string | undefined = state.ndv.input.nodeName;
const inputRunIndex: number = state.ndv.input.run ?? 0;
const inputBranchIndex: number = state.ndv.input.branch?? 0;
if (!executionData || !inputNodeName || inputRunIndex === undefined || inputBranchIndex === undefined) {
return [];
}
return executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[inputBranchIndex];
}, },
isVersionsOpen: (state: IUiState) => { isVersionsOpen: (state: IUiState) => {
return state.modals[VERSIONS_MODAL_KEY].open; return state.modals[VERSIONS_MODAL_KEY].open;
@@ -230,9 +245,19 @@ const module: Module<IUiState, IRootState> = {
mappingTelemetry: (state: IUiState) => state.ndv.mappingTelemetry, mappingTelemetry: (state: IUiState) => state.ndv.mappingTelemetry,
getCurrentView: (state: IUiState) => state.currentView, getCurrentView: (state: IUiState) => state.currentView,
isNodeView: (state: IUiState) => [VIEWS.NEW_WORKFLOW.toString(), VIEWS.WORKFLOW.toString(), VIEWS.EXECUTION.toString()].includes(state.currentView), isNodeView: (state: IUiState) => [VIEWS.NEW_WORKFLOW.toString(), VIEWS.WORKFLOW.toString(), VIEWS.EXECUTION.toString()].includes(state.currentView),
hoveringItem: (state: IUiState) => state.ndv.hoveringItem,
ndvInputNodeName: (state: IUiState) => state.ndv.input.nodeName,
ndvInputRunIndex: (state: IUiState) => state.ndv.input.run,
ndvInputBranchIndex: (state: IUiState) => state.ndv.input.branch,
getNDVDataIsEmpty: (state: IUiState) => (panel: 'input' | 'output'): boolean => state.ndv[panel].data.isEmpty, getNDVDataIsEmpty: (state: IUiState) => (panel: 'input' | 'output'): boolean => state.ndv[panel].data.isEmpty,
}, },
mutations: { mutations: {
setInputNodeName: (state: IUiState, name: string | undefined) => {
Vue.set(state.ndv.input, 'nodeName', name);
},
setInputRunIndex: (state: IUiState, run?: string) => {
Vue.set(state.ndv.input, 'run', run);
},
setMainPanelDimensions: (state: IUiState, params: { panelType:string, dimensions: { relativeLeft?: number, relativeRight?: number, relativeWidth?: number }}) => { setMainPanelDimensions: (state: IUiState, params: { panelType:string, dimensions: { relativeLeft?: number, relativeRight?: number, relativeWidth?: number }}) => {
Vue.set( Vue.set(
state.mainPanelDimensions, state.mainPanelDimensions,
@@ -337,6 +362,12 @@ const module: Module<IUiState, IRootState> = {
resetMappingTelemetry(state: IUiState) { resetMappingTelemetry(state: IUiState) {
state.ndv.mappingTelemetry = {}; state.ndv.mappingTelemetry = {};
}, },
setHoveringItem(state: IUiState, item: null | IUiState['ndv']['hoveringItem']) {
Vue.set(state.ndv, 'hoveringItem', item);
},
setNDVBranchIndex(state: IUiState, e: {pane: 'input' | 'output', branchIndex: number}) {
Vue.set(state.ndv[e.pane], 'branch', e.branchIndex);
},
setNDVPanelDataIsEmpty(state: IUiState, payload: {panel: 'input' | 'output', isEmpty: boolean}) { setNDVPanelDataIsEmpty(state: IUiState, payload: {panel: 'input' | 'output', isEmpty: boolean}) {
Vue.set(state.ndv[payload.panel].data, 'isEmpty', payload.isEmpty); Vue.set(state.ndv[payload.panel].data, 'isEmpty', payload.isEmpty);
}, },

View File

@@ -0,0 +1,159 @@
import { INodeExecutionData, IPairedItemData, IRunData, ITaskData } from "n8n-workflow";
import { IExecutionResponse, TargetItem } from "./Interface";
import { isNotNull } from "./typeGuards";
export function getPairedItemId(node: string, run: number, output: number, item: number): string {
return `${node}_r${run}_o${output}_i${item}`;
}
export function getSourceItems(data: IExecutionResponse, target: TargetItem): TargetItem[] {
if (!data?.data?.resultData?.runData) {
return [];
}
const runData = data.data.resultData.runData;
const taskData: ITaskData | undefined = runData[target.nodeName]?.[target.runIndex];
const source = taskData?.source || [];
if (source.length === 0) {
return [];
}
const item = taskData?.data?.main?.[target.outputIndex]?.[target.itemIndex];
if (!item || item.pairedItem === undefined) {
return [];
}
const pairedItem: IPairedItemData[] = Array.isArray(item.pairedItem) ? item.pairedItem : (typeof item.pairedItem === 'object' ? [item.pairedItem] : [{item: item.pairedItem}]);
const sourceItems = pairedItem.map((item) => {
const input = item.input || 0;
return {
nodeName: source?.[input]?.previousNode,
runIndex: source?.[input]?.previousNodeRun || 0,
itemIndex: item.item,
outputIndex: source[input]?.previousNodeOutput || 0,
};
});
return sourceItems.filter((item): item is TargetItem => isNotNull(item));
}
function addPairing(paths: {[item: string]: string[][]}, pairedItemId: string, pairedItem: IPairedItemData, sources: ITaskData['source']) {
paths[pairedItemId] = paths[pairedItemId] || [];
const input = pairedItem.input || 0;
const sourceNode = sources[input]?.previousNode;
if (!sourceNode) { // trigger nodes for example
paths[pairedItemId].push([pairedItemId]);
return;
}
const sourceNodeOutput = sources[input]?.previousNodeOutput || 0;
const sourceNodeRun = sources[input]?.previousNodeRun || 0;
const sourceItem = getPairedItemId(sourceNode, sourceNodeRun, sourceNodeOutput, pairedItem.item);
if (!paths[sourceItem]) {
paths[sourceItem] = [[sourceItem]]; // pinned data case
}
paths[sourceItem]?.forEach((path) => {
paths?.[pairedItemId]?.push([...path, pairedItemId]);
});
}
function addPairedItemIdsRec(node: string, runIndex: number, runData: IRunData, seen: Set<string>, paths: {[item: string]: string[][]}, pinned: Set<string>) {
const key = `${node}_r${runIndex}`;
if (seen.has(key)) {
return;
}
seen.add(key);
if (pinned.has(node)) {
return;
}
const nodeRunData = runData[node];
if (!Array.isArray(nodeRunData)) {
return;
}
const data = nodeRunData[runIndex];
if (!data?.data?.main) {
return;
}
const sources = data.source || [];
sources.forEach((source) => {
if (source?.previousNode) {
addPairedItemIdsRec(source.previousNode, source.previousNodeRun ?? 0, runData, seen, paths, pinned);
}
});
const mainData = data.data.main || [];
mainData.forEach((outputData, output: number) => {
if (!outputData) {
return;
}
outputData.forEach((executionData, item: number) => {
const pairedItemId = getPairedItemId(node, runIndex, output, item);
if (!executionData.pairedItem) {
paths[pairedItemId] = [];
return;
}
const pairedItem = executionData.pairedItem;
if (Array.isArray(pairedItem)) {
pairedItem.forEach((item) => {
addPairing(paths, pairedItemId, item, sources);
});
return;
}
if (typeof pairedItem === 'object') {
addPairing(paths, pairedItemId, pairedItem, sources);
return;
}
addPairing(paths, pairedItemId, {item: pairedItem}, sources);
});
});
}
function getMapping(paths: {[item: string]: string[][]}): {[item: string]: Set<string>} {
const mapping: {[itemId: string]: Set<string>} = {};
Object.keys(paths).forEach((item) => {
paths?.[item]?.forEach((path) => {
path.forEach((otherItem) => {
if (otherItem !== item) {
mapping[otherItem] = mapping[otherItem] || new Set();
mapping[otherItem].add(item);
mapping[item] = mapping[item] || new Set();
mapping[item].add(otherItem);
}
});
});
});
return mapping;
}
export function getPairedItemsMapping(executionResponse: IExecutionResponse | null): {[itemId: string]: Set<string>} {
if (!executionResponse?.data?.resultData?.runData) {
return {};
}
const seen = new Set<string>();
const runData = executionResponse.data.resultData.runData;
const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData || {}));
const paths: {[item: string]: string[][]} = {};
Object.keys(runData).forEach((node) => {
runData[node].forEach((_, runIndex: number) => {
addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned);
});
});
return getMapping(paths);
}

View File

@@ -124,11 +124,11 @@ export class I18nClass {
* Hint for a top-level param. * Hint for a top-level param.
*/ */
hint( hint(
{ name: parameterName, hint }: { name: string; hint: string; }, { name: parameterName, hint }: { name: string; hint?: string; },
) { ) {
return context.dynamicRender({ return context.dynamicRender({
key: `${credentialPrefix}.${parameterName}.hint`, key: `${credentialPrefix}.${parameterName}.hint`,
fallback: hint, fallback: hint || '',
}); });
}, },
@@ -174,11 +174,11 @@ export class I18nClass {
* Placeholder for a `string` param. * Placeholder for a `string` param.
*/ */
placeholder( placeholder(
{ name: parameterName, placeholder }: { name: string; placeholder: string; }, { name: parameterName, placeholder }: { name: string; placeholder?: string; },
) { ) {
return context.dynamicRender({ return context.dynamicRender({
key: `${credentialPrefix}.${parameterName}.placeholder`, key: `${credentialPrefix}.${parameterName}.placeholder`,
fallback: placeholder, fallback: placeholder || '',
}); });
}, },
}; };
@@ -247,7 +247,7 @@ export class I18nClass {
* - For a `collection` or `fixedCollection`, the placeholder is the button text. * - For a `collection` or `fixedCollection`, the placeholder is the button text.
*/ */
placeholder( placeholder(
parameter: { name: string; placeholder: string; type: string }, parameter: { name: string; placeholder?: string; type: string },
path: string, path: string,
) { ) {
let middleKey = parameter.name; let middleKey = parameter.name;
@@ -259,7 +259,7 @@ export class I18nClass {
return context.dynamicRender({ return context.dynamicRender({
key: `${initialKey}.${middleKey}.placeholder`, key: `${initialKey}.${middleKey}.placeholder`,
fallback: parameter.placeholder, fallback: parameter.placeholder || '',
}); });
}, },

View File

@@ -635,6 +635,8 @@
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts [3 min] \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=RpjQTGKm-ok)", "onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts [3 min] \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=RpjQTGKm-ok)",
"openWorkflow.workflowImportError": "Could not import workflow", "openWorkflow.workflowImportError": "Could not import workflow",
"openWorkflow.workflowNotFoundError": "Could not find workflow", "openWorkflow.workflowNotFoundError": "Could not find workflow",
"parameterInput.expressionResult": "e.g. {result}",
"parameterInput.emptyString": "[empty]",
"parameterInput.customApiCall": "Custom API Call", "parameterInput.customApiCall": "Custom API Call",
"parameterInput.error": "ERROR", "parameterInput.error": "ERROR",
"parameterInput.expression": "Expression", "parameterInput.expression": "Expression",

View File

@@ -3,7 +3,6 @@ import Vuex from 'vuex';
import { import {
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
DEFAULT_NODETYPE_VERSION,
} from '@/constants'; } from '@/constants';
import { import {
@@ -48,6 +47,7 @@ import {stringSizeInBytes} from "@/components/helpers";
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus"; import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
import communityNodes from './modules/communityNodes'; import communityNodes from './modules/communityNodes';
import { isJsonKeyObject } from './utils'; import { isJsonKeyObject } from './utils';
import { getPairedItemsMapping } from './pairedItemUtils';
Vue.use(Vuex); Vue.use(Vuex);
@@ -77,6 +77,7 @@ const state: IRootState = {
oauthCallbackUrls: {}, oauthCallbackUrls: {},
n8nMetadata: {}, n8nMetadata: {},
workflowExecutionData: null, workflowExecutionData: null,
workflowExecutionPairedItemMappings: {},
lastSelectedNode: null, lastSelectedNode: null,
lastSelectedNodeOutputIndex: null, lastSelectedNodeOutputIndex: null,
nodeViewOffsetPosition: [0, 0], nodeViewOffsetPosition: [0, 0],
@@ -379,6 +380,8 @@ export const store = new Vuex.Store({
Vue.set(state.workflow.pinData, nameData.new, state.workflow.pinData[nameData.old]); Vue.set(state.workflow.pinData, nameData.new, state.workflow.pinData[nameData.old]);
Vue.delete(state.workflow.pinData, nameData.old); Vue.delete(state.workflow.pinData, nameData.old);
} }
state.workflowExecutionPairedItemMappings = getPairedItemsMapping(state.workflowExecutionData);
}, },
resetAllNodesIssues(state) { resetAllNodesIssues(state) {
@@ -633,6 +636,7 @@ export const store = new Vuex.Store({
setWorkflowExecutionData(state, workflowResultData: IExecutionResponse | null) { setWorkflowExecutionData(state, workflowResultData: IExecutionResponse | null) {
state.workflowExecutionData = workflowResultData; state.workflowExecutionData = workflowResultData;
state.workflowExecutionPairedItemMappings = getPairedItemsMapping(state.workflowExecutionData);
}, },
addNodeExecutionData(state, pushData: IPushDataNodeExecuteAfter): void { addNodeExecutionData(state, pushData: IPushDataNodeExecuteAfter): void {
if (state.workflowExecutionData === null || !state.workflowExecutionData.data) { if (state.workflowExecutionData === null || !state.workflowExecutionData.data) {
@@ -642,6 +646,7 @@ export const store = new Vuex.Store({
Vue.set(state.workflowExecutionData.data.resultData.runData, pushData.nodeName, []); Vue.set(state.workflowExecutionData.data.resultData.runData, pushData.nodeName, []);
} }
state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data); state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
state.workflowExecutionPairedItemMappings = getPairedItemsMapping(state.workflowExecutionData);
}, },
clearNodeExecutionData(state, nodeName: string): void { clearNodeExecutionData(state, nodeName: string): void {
if (state.workflowExecutionData === null || !state.workflowExecutionData.data) { if (state.workflowExecutionData === null || !state.workflowExecutionData.data) {
@@ -709,6 +714,9 @@ export const store = new Vuex.Store({
}, },
}, },
getters: { getters: {
workflowExecutionPairedItemMappings: (state): IRootState['workflowExecutionPairedItemMappings'] => {
return state.workflowExecutionPairedItemMappings;
},
executedNode: (state): string | undefined => { executedNode: (state): string | undefined => {
return state.workflowExecutionData ? state.workflowExecutionData.executedNode : undefined; return state.workflowExecutionData ? state.workflowExecutionData.executedNode : undefined;
}, },

View File

@@ -3,3 +3,7 @@ import { INodeParameterResourceLocator } from "n8n-workflow";
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator { export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
return Boolean(typeof value === 'object' && value && 'mode' in value && 'value' in value); return Boolean(typeof value === 'object' && value && 'mode' in value && 'value' in value);
} }
export function isNotNull<T>(value: T | null): value is T {
return value !== null;
}