mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
✨ Add Templates (#2720)
* Templates Bugs / Fixed Various Bugs / Multiply Api Request, Carousel Gradient, Core Nodes Filters ... * Updated MainSidebar Paddings * N8N-Templates Bugfixing - Remove Unnecesairy Icon (Shape), Refatctor infiniteScrollEnabled Prop + updated infiniterScroll functinality * N8N-2853 Fixed Carousel Arrows Bug after Cleaning the SearchBar * fix telemetry init * fix search tracking issues * N8N-2853 Created FilterTemplateNode Constant Array, Filter PlayButton and WebhookRespond from Nodes, Added Box for showing more nodes inside TemplateList, Updated NewWorkflowButton to primary, Fixed Markdown issue with Code * N8N-2853 Removed Placeholder if Workflows Or Collections are not found, Updated the Logic * fix telemetry events * clean up session id * update user inserted event * N8N-2853 Fixed Categories to Moving if the names are long * Add todos * Update Routes on loading * fix spacing * Update Border Color * Update Border Readius * fix filter fn * fix constant, console error * N8N-2853 PR Fixes, Refactoring, Removing unnecesairy code .. * N8N-2853 PR Fixes - Editor-ui Fixes, Refactoring, Removing Dead Code ... * N8N-2853 Refactor Card to LongCard * clean up spacing, replace css var * clean up spacing * set categories as optional in node * replace vars * refactor store * remove unnesssary import * fix error * fix templates view to start * add to cache * fix coll view data * fix categories * fix category event * fix collections carousel * fix initial load and search * fix infinite load * fix query param * fix scrolling issues * fix scroll to top * fix search * fix collections search * fix navigation bug * rename view * update package lock * rename workflow view * rename coll view * update routes * add wrapper component * set session id * fix search tracking * fix session tracking * remove deleted mutation * remove check for unsupported nodes * refactor filters * lazy load template * clean up types * refactor infinte scroll * fix end of search * Fix spacing * fix coll loading * fix types * fix coll view list * fix navigation * rename types * rename state * fix search responsiveness * fix coll view spacing * fix search view spacing * clean up views * set background color * center page not vert * fix workflow view * remove import * fix background color * fix background * clean props * clean up imports * refactor button * update background color * fix spacing issue * rename event * update telemetry event * update endpoints, add loading view, check for endpoint health * remove conolse log * N8N-2853 Fixed Menu Items Padding * replace endpoints * fix type issues * fix categories * N8N-2853 Fixed ParameterInput Placeholder after ElementUI Upgrade * update createdAt * ⚡ Fix placeholder in creds config modal * ✏️ Adjust docstring to `credText` placeholder version * N8N-2853 Optimized * N8N-2853 Optimized code * ⚡ Add deployment type to FE settings * ⚡ Add deployment type to interfaces * N8N-2853 Removed Animated prop from components * ⚡ Add deployment type to store module * ✨ Create hiring banner * ⚡ Display hiring banner * ⏪ Undo unrelated change * N8N-2853 Refactor TemplateFilters * ⚡ Fix indentation * N8N-2853 Reorder items / TemplateList * 👕 Fix lint * N8N-2853 Refactor TemplateFilters Component * N8N-2853 Reorder TemplateList * refactor template card * update timeout * fix removelistener * fix spacing * split enabled from offline * add spacing to go back * N8N-2853 Fixed Screens for Tablet & Mobile * N8N-2853 Update Stores Order * remove image componet * remove placeholder changes * N8N-2853 Fixed Chinnese Placeholders for El Select Component that comes from the Library Upgrade * N8N-2853 Fixed Vue Agile Console Warnings * N8N-2853 Update Collection Route * ✏️ Update jobs URL * 🚚 Move logging to root component * ⚡ Refactor `deploymentType` to `isInternalUser` * ⚡ Improve syntax * fix cut bug in readonly view * N8N-3012 Fixed Details section in templates with lots of description, Fixed Mardown Block with overflox-x * N8N-3012 Increased Font-size, Spacing and Line-height of the Categories Items * N8N-3012 Fixed Vue-agile client width error on resize * only delay redirect for root path * N8N-3012 Fixed Carousel Arrows that Disappear * N8N-3012 Make Loading Screen same color as Templates * N8N-3012 Markdown renders inline block as block code * add offline warning * hide log from workflow iframe * update text * make search button larger * N8N-3012 Categories / Tags extended all the way in details section * load data in cred modals * remove deleted message * add external hook * remove import * update env variable description * fix markdown width issue * disable telemetry for demo, add session id to template pages * fix telemetery bugs * N8N-3012 Not found Collections/Wokrkflow * N8N-3012 Checkboxes change order when categories are changed * N8N-3012 Refactor SortedCategories inside TemplateFilters component * fix firefox bug * add telemetry requirements * add error check * N8N-3012 Update GoBackButton to check if Route History is present * N8N-3012 Fixed WF Nodes Icons * hide workflow screenshots * remove unnessary mixins * rename prop * fix design a bit * rename data * clear workspace on destroy * fix copy paste bug * fix disabled state * N8N-3012 Fixed Saving/Leave without saving Modal * fix telemetry issue * fix telemetry issues, error bug * fix error notification * disable workflow menu items on templates * fix i18n elementui issue * Remove Emit - NodeType from HoverableNodeIcon component * TechnicalFixes: NavigateTo passed down as function should be helper * TechnicalFixes: Update NavigateTo function * TechnicalFixes: Add FilterCoreNodes directly as function * check for empty connecitions * fix titles * respect new lines * increase categories to be sliced * rename prop * onUseWorkflow * refactor click event * fix bug, refactor * fix loading story * add default * fix styles at right level of abstraction * add wrapper with width * remove loading blocks component * add story * rename prop * fix spacing * refactor tag, add story * move margin to container * fix tag redirect, remove unnessary check * make version optional * rename view * move from workflows to templates store * remove unnessary change * remove unnessary css * rename component * refactor collection card * add boolean to prevent shrink * clean up carousel * fix redirection bug on save * remove listeners to fix multiple listeners bug * remove unnessary types * clean up boolean set * fix node select bug * rename component * remove unnessary class * fix redirection bug * remove unnessary error * fix typo * fix blockquotes, pre * refactor markdown rendering * remove console log * escape markdown * fix safari bug * load active workflows to fix modal bug * ⬆️ Update package-lock.json file * ⚡ Add n8n version as header Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
401e626a64
commit
cfa91cda27
523
package-lock.json
generated
523
package-lock.json
generated
|
@ -300,9 +300,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz",
|
||||
"integrity": "sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ==",
|
||||
"version": "7.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz",
|
||||
"integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.16.7",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
|
@ -396,9 +396,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz",
|
||||
"integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==",
|
||||
"version": "7.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.6.tgz",
|
||||
"integrity": "sha512-2ULmRdqoOMpdvkbT8jONrZML/XALfzxlb052bldftkicAUy8AxSCkD5trDPQcwHNmolcl7wP6ehNqMlyUw6AaA==",
|
||||
"requires": {
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-module-imports": "^7.16.7",
|
||||
|
@ -406,8 +406,8 @@
|
|||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"@babel/template": "^7.16.7",
|
||||
"@babel/traverse": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
"@babel/traverse": "^7.17.3",
|
||||
"@babel/types": "^7.17.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
|
@ -553,11 +553,11 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-static-block": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz",
|
||||
"integrity": "sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw==",
|
||||
"version": "7.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz",
|
||||
"integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==",
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.16.7",
|
||||
"@babel/helper-create-class-features-plugin": "^7.17.6",
|
||||
"@babel/helper-plugin-utils": "^7.16.7",
|
||||
"@babel/plugin-syntax-class-static-block": "^7.14.5"
|
||||
}
|
||||
|
@ -9930,14 +9930,14 @@
|
|||
"integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw=="
|
||||
},
|
||||
"@oclif/parser": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.6.tgz",
|
||||
"integrity": "sha512-tXb0NKgSgNxmf6baN6naK+CCwOueaFk93FG9u202U7mTBHUKsioOUlw1SG/iPi9aJM3WE4pHLXmty59pci0OEw==",
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.7.tgz",
|
||||
"integrity": "sha512-b11xBmIUK+LuuwVGJpFs4LwQN2xj2cBWj2c4z1FtiXGrJ85h9xV6q+k136Hw0tGg1jQoRXuvuBnqQ7es7vO9/Q==",
|
||||
"requires": {
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@oclif/errors": "^1.3.5",
|
||||
"@oclif/linewrap": "^1.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
|
@ -10812,6 +10812,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
||||
},
|
||||
"braces": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||
|
@ -11041,10 +11046,11 @@
|
|||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.11.0.tgz",
|
||||
"integrity": "sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A==",
|
||||
"requires": {
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
|
@ -11722,6 +11728,11 @@
|
|||
"webpack-virtual-modules": "^0.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
|
@ -11900,10 +11911,11 @@
|
|||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.11.0.tgz",
|
||||
"integrity": "sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A==",
|
||||
"requires": {
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
|
@ -12592,15 +12604,20 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA=="
|
||||
},
|
||||
"@types/localtunnel": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/localtunnel/-/localtunnel-1.9.0.tgz",
|
||||
"integrity": "sha512-3YxO7RHRrmtYNX6Rhkr97bnXHrF1Ckfo4axENWLcBXWi+8B1WsNbqPqe5Eg6TA5survjAWWvLTu1KQesuLHVgQ=="
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.178",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
|
||||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
|
||||
"version": "4.14.179",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz",
|
||||
"integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w=="
|
||||
},
|
||||
"@types/lodash.camelcase": {
|
||||
"version": "4.3.6",
|
||||
|
@ -12647,6 +12664,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||
"requires": {
|
||||
"@types/linkify-it": "*",
|
||||
"@types/mdurl": "*"
|
||||
}
|
||||
},
|
||||
"@types/mdast": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
||||
|
@ -12655,6 +12681,11 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"@types/mdurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA=="
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
|
@ -13646,15 +13677,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"array-union": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||
|
@ -13690,21 +13712,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"optional": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
|
@ -13767,58 +13774,6 @@
|
|||
"worker-rpc": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
|
||||
"integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"at-least-node": "^1.0.0",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
|
||||
|
@ -13853,12 +13808,6 @@
|
|||
"slash": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"optional": true
|
||||
},
|
||||
"ignore": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
|
||||
|
@ -13887,16 +13836,6 @@
|
|||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||
|
@ -13932,17 +13871,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
|
@ -13953,15 +13881,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
|
||||
|
@ -14055,12 +13974,6 @@
|
|||
"requires": {
|
||||
"tslib": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -14869,11 +14782,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"postcss": {
|
||||
"version": "8.4.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz",
|
||||
"integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==",
|
||||
"version": "8.4.7",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz",
|
||||
"integrity": "sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==",
|
||||
"requires": {
|
||||
"nanoid": "^3.2.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
|
@ -16000,9 +15913,9 @@
|
|||
"integrity": "sha512-uUbetCWczQHbsKyX1C99XpQHBM8SWfovvaZhPIj23/1uV7SQf0WeRZbiLpw0JZm+LHTChfNgrLfDJOVoU2kU+A=="
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.1077.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1077.0.tgz",
|
||||
"integrity": "sha512-orJvJROs8hJaQRfHsX7Zl5PxEgrD/uTXyqXz9Yu9Io5VVxzvnOty9oHmvEMSlgTIf1qd01gnev/vpvP1HgzKtw==",
|
||||
"version": "2.1082.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1082.0.tgz",
|
||||
"integrity": "sha512-aDrUZ63O/ocuC827ursDqFQAm3jhqsJu1DvMCCFg73y+FK9pXXNHp2mwdi3UeeHvtfxISCLCjuyO3VFd/tpVfA==",
|
||||
"requires": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
|
@ -20356,6 +20269,11 @@
|
|||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
|
||||
},
|
||||
"cssfilter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
|
||||
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
|
||||
},
|
||||
"cssnano": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
|
||||
|
@ -21426,9 +21344,9 @@
|
|||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.71",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz",
|
||||
"integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw=="
|
||||
"version": "1.4.73",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.73.tgz",
|
||||
"integrity": "sha512-RlCffXkE/LliqfA5m29+dVDPB2r72y2D2egMMfIy3Le8ODrxjuZNVo4NIC2yPL01N4xb4nZQLwzi6Z5tGIGLnA=="
|
||||
},
|
||||
"element-resize-detector": {
|
||||
"version": "1.2.4",
|
||||
|
@ -21439,9 +21357,9 @@
|
|||
}
|
||||
},
|
||||
"element-ui": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.13.2.tgz",
|
||||
"integrity": "sha512-r761DRPssMPKDiJZWFlG+4e4vr0cRG/atKr3Eqr8Xi0tQMNbtmYU1QXvFnKiFPFFGkgJ6zS6ASkG+sellcoHlQ==",
|
||||
"version": "2.15.7",
|
||||
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.15.7.tgz",
|
||||
"integrity": "sha512-+J6rnXajxzLwV6w8Q6bf7Yqzk1FO1ewbIrCy/4B5alnd7tj8WEpfQoAvISirVaUGVGy77d9Ji3o2bF4f0AsJLQ==",
|
||||
"requires": {
|
||||
"async-validator": "~1.8.1",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.0",
|
||||
|
@ -23534,6 +23452,124 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
|
||||
"integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"optional": true
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"at-least-node": "^1.0.0",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"optional": true
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6",
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
|
@ -31263,11 +31299,6 @@
|
|||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"json3": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
|
||||
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
|
@ -32042,6 +32073,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
},
|
||||
"lodash.orderby": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz",
|
||||
"integrity": "sha1-5pfwTOXXhSL1TZM4syuBozk+TrM="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
|
@ -32071,6 +32107,11 @@
|
|||
"lodash._reinterpolate": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.transform": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||
|
@ -32437,6 +32478,45 @@
|
|||
"resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
|
||||
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it-emoji": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz",
|
||||
"integrity": "sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ=="
|
||||
},
|
||||
"markdown-it-link-attributes": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.0.tgz",
|
||||
"integrity": "sha512-ssjxSLlLfQBkX6BvAx1rCPrx7ZoK91llQQvS3P7KXvlbnVD34OUkfXwWecN7su/7mrI/HOW0RI5szdJOIqYC3w=="
|
||||
},
|
||||
"markdown-it-task-lists": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
|
||||
},
|
||||
"markdown-to-jsx": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.6.tgz",
|
||||
|
@ -33245,11 +33325,11 @@
|
|||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mssql": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-6.4.0.tgz",
|
||||
"integrity": "sha512-Mtgu3PXqoaL7aHCMurttvEHibjvz5XKjlR6ZCDyAeKtDBORpxm88JyzEU2EESVf7588GulYKc7Gr+Txf5CICBQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-6.4.1.tgz",
|
||||
"integrity": "sha512-G1I7mM0gfxcH5TGSNoVmxq13Mve5YnQgRAlonqaMlHEjHjMn1g04bsrIQbVHFRdI6++dw/FGWlh8GoItJMoUDw==",
|
||||
"requires": {
|
||||
"debug": "^4.3.2",
|
||||
"debug": "^4.3.3",
|
||||
"tarn": "^1.1.5",
|
||||
"tedious": "^6.7.1"
|
||||
}
|
||||
|
@ -37481,9 +37561,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"history": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
|
||||
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
|
@ -37500,9 +37580,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"history": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
|
||||
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
|
@ -37814,20 +37894,13 @@
|
|||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
|
||||
},
|
||||
"refractor": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.5.0.tgz",
|
||||
"integrity": "sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
|
||||
"requires": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.25.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prismjs": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
|
||||
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
|
||||
}
|
||||
"prismjs": "~1.27.0"
|
||||
}
|
||||
},
|
||||
"regenerate": {
|
||||
|
@ -38244,9 +38317,9 @@
|
|||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
"integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA=="
|
||||
"version": "21.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
|
||||
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -38744,9 +38817,9 @@
|
|||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.49.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
|
||||
"integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
|
@ -39475,16 +39548,15 @@
|
|||
}
|
||||
},
|
||||
"sockjs-client": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz",
|
||||
"integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.0.tgz",
|
||||
"integrity": "sha512-qVHJlyfdHFht3eBFZdKEXKTlb7I4IV41xnVNo8yUKA1UHcPJwgW2SvTq9LhnjjCywSkSK7c/e4nghU0GOoMCRQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"eventsource": "^1.0.7",
|
||||
"faye-websocket": "^0.11.3",
|
||||
"debug": "^3.2.7",
|
||||
"eventsource": "^1.1.0",
|
||||
"faye-websocket": "^0.11.4",
|
||||
"inherits": "^2.0.4",
|
||||
"json3": "^3.3.3",
|
||||
"url-parse": "^1.5.3"
|
||||
"url-parse": "^1.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
|
@ -41482,9 +41554,9 @@
|
|||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.1.tgz",
|
||||
"integrity": "sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==",
|
||||
"version": "3.15.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.2.tgz",
|
||||
"integrity": "sha512-peeoTk3hSwYdoc9nrdiEJk+gx1ALCtTjdYuKSXMTDqq7n1W7dHPqWDdSi+BPL0ni2YMeHD7hKUSdbj3TZauY2A==",
|
||||
"optional": true
|
||||
},
|
||||
"uid-number": {
|
||||
|
@ -41986,9 +42058,9 @@
|
|||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.8.tgz",
|
||||
"integrity": "sha512-9JZ5zDrn9wJoOy/t+rH00HHejbU8dq9VsOYVu272TYDrCiyVAgHKUSpPh3ruZIpv8PMVR+NXLZvfRPJv8xAcQw==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
@ -42339,9 +42411,9 @@
|
|||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
|
||||
},
|
||||
"vm2": {
|
||||
"version": "3.9.8",
|
||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.8.tgz",
|
||||
"integrity": "sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==",
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.9.tgz",
|
||||
"integrity": "sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==",
|
||||
"requires": {
|
||||
"acorn": "^8.7.0",
|
||||
"acorn-walk": "^8.2.0"
|
||||
|
@ -42369,6 +42441,15 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
||||
},
|
||||
"vue-agile": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-agile/-/vue-agile-2.0.0.tgz",
|
||||
"integrity": "sha512-5xkSLJQNRdQ7qpEnXj5FgLg33XKRHaTZKGP5qkvteOc/uGJX89MYCjPSgdNqJ1GYFGfdGAp0jvhihW8OMuXS3g==",
|
||||
"requires": {
|
||||
"lodash.orderby": "^4.6.0",
|
||||
"lodash.throttle": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"vue-class-component": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",
|
||||
|
@ -42383,9 +42464,9 @@
|
|||
}
|
||||
},
|
||||
"vue-docgen-api": {
|
||||
"version": "4.44.15",
|
||||
"resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.44.15.tgz",
|
||||
"integrity": "sha512-JBFe4EAUSmRqRHaNNHqDo1U+w1HRaHh00C0bYKE65HdN9QS6pCJCUBwi1blow0beDzLTAJYCa90xwG61WYBo4A==",
|
||||
"version": "4.44.17",
|
||||
"resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.44.17.tgz",
|
||||
"integrity": "sha512-bU1V9gvXDv5GPaOmXYcnHrc3EjwRxZ8IKFE+Hk7QWpnI5/MYriyt7rf/g/z0JS8u0vGdiYqqUVx6CB+Ghmkm5g==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.13.12",
|
||||
"@babel/types": "^7.13.12",
|
||||
|
@ -42397,7 +42478,7 @@
|
|||
"pug": "^3.0.2",
|
||||
"recast": "0.20.5",
|
||||
"ts-map": "^1.0.3",
|
||||
"vue-inbrowser-compiler-utils": "^4.44.15"
|
||||
"vue-inbrowser-compiler-utils": "^4.44.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
|
@ -42473,9 +42554,9 @@
|
|||
"integrity": "sha512-SX35iJHL5PJ4Gfh0Mo/q0shyHiI2V6Zkh51c+k8E9O1RKv5BQyYrCxRzpvPrsIOJEnLaeiovet3dsUB0e/kDzw=="
|
||||
},
|
||||
"vue-inbrowser-compiler-utils": {
|
||||
"version": "4.44.15",
|
||||
"resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.44.15.tgz",
|
||||
"integrity": "sha512-dbuZbFNl7q3+MjLyFxD14LnrbYuhexVCbCU9AFJ2zd3zqHrueXSYGzYTLLTZ++fnCMC3J60xe409e/KEft+Cbw==",
|
||||
"version": "4.44.17",
|
||||
"resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.44.17.tgz",
|
||||
"integrity": "sha512-dvxumVgIzR4FXjAWYWIOnpD+6bW0dLkoAv43UShER8gVIhLFo9UEmbF31wD6YWJj94lUpbVIuWl2qc6axYNEAQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.3.1"
|
||||
}
|
||||
|
@ -43914,6 +43995,22 @@
|
|||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
|
||||
"integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM="
|
||||
},
|
||||
"xss": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz",
|
||||
"integrity": "sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw==",
|
||||
"requires": {
|
||||
"commander": "^2.20.3",
|
||||
"cssfilter": "0.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
@ -690,6 +690,21 @@ const config = convict({
|
|||
},
|
||||
},
|
||||
|
||||
templates: {
|
||||
enabled: {
|
||||
doc: 'Whether templates feature is enabled to load workflow templates.',
|
||||
format: Boolean,
|
||||
default: true,
|
||||
env: 'N8N_TEMPLATES_ENABLED',
|
||||
},
|
||||
host: {
|
||||
doc: 'Endpoint host to retrieve workflow templates from endpoints.',
|
||||
format: String,
|
||||
default: 'https://api.n8n.io/',
|
||||
env: 'N8N_TEMPLATES_HOST',
|
||||
},
|
||||
},
|
||||
|
||||
binaryDataManager: {
|
||||
availableModes: {
|
||||
format: String,
|
||||
|
|
|
@ -412,6 +412,11 @@ export interface IN8nUISettings {
|
|||
personalizationSurvey: IPersonalizationSurvey;
|
||||
defaultLocale: string;
|
||||
logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
||||
deploymentType: string;
|
||||
templates: {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPersonalizationSurveyAnswers {
|
||||
|
|
|
@ -286,6 +286,11 @@ class App {
|
|||
},
|
||||
defaultLocale: config.get('defaultLocale'),
|
||||
logLevel: config.get('logs.level'),
|
||||
deploymentType: config.get('deployment.type'),
|
||||
templates: {
|
||||
enabled: config.get('templates.enabled'),
|
||||
host: config.get('templates.host'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCode,
|
||||
faCodeBranch,
|
||||
faCog,
|
||||
|
@ -100,6 +102,8 @@ library.add(faCalendar);
|
|||
library.add(faCheck);
|
||||
library.add(faChevronDown);
|
||||
library.add(faChevronUp);
|
||||
library.add(faChevronLeft);
|
||||
library.add(faChevronRight);
|
||||
library.add(faCode);
|
||||
library.add(faCodeBranch);
|
||||
library.add(faCog);
|
||||
|
|
|
@ -30,14 +30,14 @@
|
|||
"@fortawesome/free-solid-svg-icons": "5.x",
|
||||
"@fortawesome/vue-fontawesome": "2.x",
|
||||
"core-js": "3.x",
|
||||
"element-ui": "2.13.x"
|
||||
"element-ui": "2.15.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||
"core-js": "^3.6.5",
|
||||
"element-ui": "~2.13.0",
|
||||
"element-ui": "2.15.x",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
|
@ -48,6 +48,7 @@
|
|||
"@storybook/addon-links": "^6.3.6",
|
||||
"@storybook/vue": "^6.3.6",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
|
@ -63,6 +64,10 @@
|
|||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-vue": "^7.16.0",
|
||||
"gulp": "^4.0.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"markdown-it-link-attributes": "^4.0.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prettier": "^2.3.2",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
|
@ -74,6 +79,7 @@
|
|||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-dart-sass": "^1.0.2",
|
||||
"node-notifier": ">=8.0.1",
|
||||
"trim": ">=0.0.3"
|
||||
"trim": ">=0.0.3",
|
||||
"xss": "^1.0.10"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import N8nLoading from './Loading.vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Loading',
|
||||
component: N8nLoading,
|
||||
argTypes: {
|
||||
animated: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
rows: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: [1, 2, 3, 4, 5],
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['button', 'h1', 'image', 'p'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nLoading,
|
||||
},
|
||||
template: '<n8n-loading v-bind="$props"></n8n-loading>',
|
||||
});
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
variant: 'p',
|
||||
};
|
86
packages/design-system/src/components/N8nLoading/Loading.vue
Normal file
86
packages/design-system/src/components/N8nLoading/Loading.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<el-skeleton :loading="loading" :animated="animated">
|
||||
<template slot="template">
|
||||
<el-skeleton-item
|
||||
v-if="variant === 'button'"
|
||||
:variant="variant"
|
||||
/>
|
||||
|
||||
<div v-if="variant === 'h1'">
|
||||
<div
|
||||
v-for="(item, index) in rows"
|
||||
:key="index"
|
||||
:class="{
|
||||
[$style.h1Last]: item === rows && rows > 1 && shrinkLast,
|
||||
}"
|
||||
>
|
||||
<el-skeleton-item
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-skeleton-item
|
||||
v-if="variant === 'image'"
|
||||
:variant="variant"
|
||||
/>
|
||||
<div v-if="variant === 'p'">
|
||||
<div
|
||||
v-for="(item, index) in rows"
|
||||
:key="index"
|
||||
:class="{
|
||||
[$style.pLast]: item === rows && rows > 1 && shrinkLast,
|
||||
}">
|
||||
<el-skeleton-item
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ElSkeleton from 'element-ui/lib/skeleton';
|
||||
import ElSkeletonItem from 'element-ui/lib/skeleton-item';
|
||||
|
||||
export default {
|
||||
name: 'n8n-loading',
|
||||
components: {
|
||||
ElSkeleton,
|
||||
ElSkeletonItem,
|
||||
},
|
||||
props: {
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
shrinkLast: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'p',
|
||||
validator: (value: string): boolean => ['p', 'h1', 'button', 'image'].includes(value),
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.h1Last {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.pLast {
|
||||
width: 61%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nLoading from './Loading.vue';
|
||||
|
||||
export default N8nLoading;
|
|
@ -0,0 +1,48 @@
|
|||
import N8nMarkdown from './Markdown.vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Markdown',
|
||||
component: N8nMarkdown,
|
||||
argTypes: {
|
||||
content: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
loadingBlocks: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: [1, 2, 3, 4, 5],
|
||||
},
|
||||
},
|
||||
loadingRows: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: [1, 2, 3, 4, 5],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nMarkdown,
|
||||
},
|
||||
template: '<n8n-markdown v-bind="$props"></n8n-markdown>',
|
||||
});
|
||||
|
||||
export const Markdown = Template.bind({});
|
||||
Markdown.args = {
|
||||
content: `I wanted a system to monitor website content changes and notify me. So I made it using n8n.\n\nEspecially my competitor blogs. I wanted to know how often they are posting new articles. (I used their sitemap.xml file) (The below workflow may vary)\n\nIn the Below example, I used HackerNews for example.\n\nExplanation:\n\n- First HTTP Request node crawls the webpage and grabs the website source code\n- Then wait for x minutes\n- Again, HTTP Node crawls the webpage\n- If Node compares both results are equal if anything is changed. It’ll go to the false branch and notify me in telegram.\n\n**Workflow:**\n\n![](fileId:1)\n\n**Sample Response:**\n\n![](https://community.n8n.io/uploads/default/original/2X/d/d21ba41d7ac9ff5cd8148fedb07d0f1ff53b2529.png)\n`,
|
||||
loading: false,
|
||||
images: [{
|
||||
id: 1,
|
||||
url: 'https://community.n8n.io/uploads/default/optimized/2X/b/b737a95de4dfe0825d50ca098171e9f33a459e74_2_690x288.png',
|
||||
}],
|
||||
};
|
220
packages/design-system/src/components/N8nMarkdown/Markdown.vue
Normal file
220
packages/design-system/src/components/N8nMarkdown/Markdown.vue
Normal file
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" ref="editor" :class="$style.markdown" v-html="htmlContent" />
|
||||
<div v-else :class="$style.markdown">
|
||||
<div v-for="(block, index) in loadingBlocks"
|
||||
:key="index">
|
||||
<n8n-loading
|
||||
:loading="loading"
|
||||
:rows="loadingRows"
|
||||
animated
|
||||
variant="p"
|
||||
/>
|
||||
<div :class="$style.spacer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import N8nLoading from '../N8nLoading';
|
||||
import Markdown from 'markdown-it';
|
||||
const markdownLink = require('markdown-it-link-attributes');
|
||||
const markdownEmoji = require('markdown-it-emoji');
|
||||
const markdownTasklists = require('markdown-it-task-lists');
|
||||
|
||||
import xss from 'xss';
|
||||
import { escapeMarkdown } from '../../utils/markdown';
|
||||
|
||||
const DEFAULT_OPTIONS_MARKDOWN = {
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true,
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS_LINK_ATTRIBUTES = {
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS_TASKLISTS = {
|
||||
label: true,
|
||||
labelAfter: true,
|
||||
};
|
||||
|
||||
interface IImage {
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
N8nLoading,
|
||||
},
|
||||
name: 'n8n-markdown',
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
loadingBlocks: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
loadingRows: {
|
||||
type: Number,
|
||||
default: () => {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
markdown: DEFAULT_OPTIONS_MARKDOWN,
|
||||
linkAttributes: DEFAULT_OPTIONS_LINK_ATTRIBUTES,
|
||||
tasklists: DEFAULT_OPTIONS_TASKLISTS,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
htmlContent(): string {
|
||||
if (!this.content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const imageUrls: { [key: string]: string } = {};
|
||||
if (this.images) {
|
||||
// @ts-ignore
|
||||
this.images.forEach((image: IImage) => {
|
||||
if (!image) {
|
||||
// Happens if an image got deleted but the workflow
|
||||
// still has a reference to it
|
||||
return;
|
||||
}
|
||||
imageUrls[image.id] = image.url;
|
||||
});
|
||||
}
|
||||
|
||||
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
||||
const html = this.md.render(escapeMarkdown(this.content));
|
||||
const safeHtml = xss(html, {
|
||||
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
||||
if (tag === 'img' && name === 'src') {
|
||||
if (value.match(fileIdRegex)) {
|
||||
const id = value.split('fileId:')[1];
|
||||
return `src=${xss.friendlyAttrValue(imageUrls[id])}` || '';
|
||||
}
|
||||
if (!value.startsWith('https://')) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Return nothing, means keep the default handling measure
|
||||
},
|
||||
onTag: function (tag, html, options) {
|
||||
if (tag === 'img' && html.includes(`alt="workflow-screenshot"`)) {
|
||||
return '';
|
||||
}
|
||||
// return nothing, keep tag
|
||||
},
|
||||
});
|
||||
|
||||
return safeHtml;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
md: new Markdown(this.options.markdown)
|
||||
.use(markdownLink, this.options.linkAttributes)
|
||||
.use(markdownEmoji)
|
||||
.use(markdownTasklists, this.options.tasklists),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.markdown {
|
||||
color: var(--color-text-base);
|
||||
|
||||
* {
|
||||
font-size: var(--font-size-m);
|
||||
line-height: var(--font-line-height-xloose);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin-bottom: var(--spacing-s);
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: var(--spacing-s);
|
||||
padding-left: var(--spacing-m);
|
||||
|
||||
li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: var(--spacing-s);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: var(--spacing-s);
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-background-base);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
li > code,
|
||||
p > code {
|
||||
padding: 0 var(--spacing-4xs);
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: cover;
|
||||
border: var(--border-width-base) var(--color-foreground-base) var(--border-style-base);
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 10px;
|
||||
font-style: italic;
|
||||
border-left: var(--border-color-base) 2px solid;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: var(--spacing-2xl);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nMarkdown from './Markdown.vue';
|
||||
|
||||
export default N8nMarkdown;
|
27
packages/design-system/src/components/N8nTag/Tag.stories.js
Normal file
27
packages/design-system/src/components/N8nTag/Tag.stories.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import N8nTag from './Tag.vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Tag',
|
||||
component: N8nTag,
|
||||
argTypes: {
|
||||
text: {
|
||||
control: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nTag,
|
||||
},
|
||||
template:
|
||||
'<n8n-tag v-bind="$props"></n8n-tag>',
|
||||
});
|
||||
|
||||
export const Tag = Template.bind({});
|
||||
Tag.args = {
|
||||
text: 'tag name',
|
||||
};
|
25
packages/design-system/src/components/N8nTag/Tag.vue
Normal file
25
packages/design-system/src/components/N8nTag/Tag.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template functional>
|
||||
<span :class="$style.tag" v-text="props.text" @click="(e) => listeners.click && listeners.click(e)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'n8n-tag',
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tag {
|
||||
min-width: max-content;
|
||||
padding: var(--spacing-4xs);
|
||||
background-color: var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
3
packages/design-system/src/components/N8nTag/index.js
Normal file
3
packages/design-system/src/components/N8nTag/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Tag from './Tag.vue';
|
||||
|
||||
export default Tag;
|
|
@ -0,0 +1,35 @@
|
|||
import N8nTags from './Tags.vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Tags',
|
||||
component: N8nTags,
|
||||
argTypes: {
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nTags,
|
||||
},
|
||||
template:
|
||||
'<n8n-tags v-bind="$props"></n8n-tags>',
|
||||
});
|
||||
|
||||
export const Tags = Template.bind({});
|
||||
Tags.args = {
|
||||
tags: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'very long tag name',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'tag1',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'tag2 yo',
|
||||
},
|
||||
],
|
||||
};
|
32
packages/design-system/src/components/N8nTags/Tags.vue
Normal file
32
packages/design-system/src/components/N8nTags/Tags.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template functional>
|
||||
<div :class="$style.tags">
|
||||
<component :is="$options.components.N8nTag" v-for="tag in props.tags" :key="tag.id" :text="tag.name" @click="(e) => listeners.click && listeners.click(tag.id, e)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import N8nTag from '../N8nTag';
|
||||
|
||||
export default {
|
||||
name: 'n8n-tags',
|
||||
components: {
|
||||
N8nTag,
|
||||
},
|
||||
props: {
|
||||
tags: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
* {
|
||||
margin: 0 var(--spacing-4xs) var(--spacing-4xs) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
3
packages/design-system/src/components/N8nTags/index.js
Normal file
3
packages/design-system/src/components/N8nTags/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Tags from './Tags.vue';
|
||||
|
||||
export default Tags;
|
|
@ -5,12 +5,16 @@ import N8nInput from './N8nInput';
|
|||
import N8nInfoTip from './N8nInfoTip';
|
||||
import N8nInputNumber from './N8nInputNumber';
|
||||
import N8nInputLabel from './N8nInputLabel';
|
||||
import N8nLoading from './N8nLoading';
|
||||
import N8nHeading from './N8nHeading';
|
||||
import N8nMarkdown from './N8nMarkdown';
|
||||
import N8nMenu from './N8nMenu';
|
||||
import N8nMenuItem from './N8nMenuItem';
|
||||
import N8nSelect from './N8nSelect';
|
||||
import N8nSpinner from './N8nSpinner';
|
||||
import N8nSquareButton from './N8nSquareButton';
|
||||
import N8nTags from './N8nTags';
|
||||
import N8nTag from './N8nTag';
|
||||
import N8nText from './N8nText';
|
||||
import N8nTooltip from './N8nTooltip';
|
||||
import N8nOption from './N8nOption';
|
||||
|
@ -23,12 +27,16 @@ export {
|
|||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nInputNumber,
|
||||
N8nLoading,
|
||||
N8nMarkdown,
|
||||
N8nHeading,
|
||||
N8nMenu,
|
||||
N8nMenuItem,
|
||||
N8nSelect,
|
||||
N8nSpinner,
|
||||
N8nSquareButton,
|
||||
N8nTags,
|
||||
N8nTag,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
N8nOption,
|
||||
|
|
|
@ -6,4 +6,6 @@ declare module 'element-ui/lib/select';
|
|||
declare module 'element-ui/lib/option';
|
||||
declare module 'element-ui/lib/menu';
|
||||
declare module 'element-ui/lib/menu-item';
|
||||
declare module 'element-ui/lib/skeleton';
|
||||
declare module 'element-ui/lib/skeleton-item';
|
||||
|
||||
|
|
12
packages/design-system/src/utils/markdown.ts
Normal file
12
packages/design-system/src/utils/markdown.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const escapeMarkdown = (html: string | undefined): string => {
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
const escaped = html.replace(/</g, "<").replace(/>/g, ">");
|
||||
// unescape greater than quotes at start of line
|
||||
const withQuotes = escaped.replace(/^((\s)*(>)+)+\s*/gm, (matches) => {
|
||||
return matches.replace(/>/g, '>');
|
||||
});
|
||||
|
||||
return withQuotes;
|
||||
};
|
|
@ -22,6 +22,7 @@
|
|||
// @use "./checkbox-group.scss";
|
||||
@use "./switch.scss";
|
||||
@use "./select.scss";
|
||||
@use "./skeleton.scss";
|
||||
@use "./button.scss";
|
||||
// @use "./button-group.scss";
|
||||
@use "./table.scss";
|
||||
|
|
82
packages/design-system/theme/src/skeleton.scss
vendored
Normal file
82
packages/design-system/theme/src/skeleton.scss
vendored
Normal file
|
@ -0,0 +1,82 @@
|
|||
.el-skeleton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-skeleton__item {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
border-radius: var(--border-radius-large);
|
||||
background: var(--color-background-base);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.el-skeleton__button {
|
||||
width: 162px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-skeleton__p {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.el-skeleton__h1 {
|
||||
height: 20px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.el-skeleton__image {
|
||||
width: unset;
|
||||
height: 500px !important;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.el-skeleton__image svg {
|
||||
width: 22%;
|
||||
height: 22%;
|
||||
fill: var(--color-info-tint-1);
|
||||
}
|
||||
|
||||
.el-skeleton__first-line,
|
||||
.el-skeleton__paragraph {
|
||||
background: var(--color-background-base);
|
||||
}
|
||||
|
||||
.el-skeleton.is-animated .el-skeleton__item {
|
||||
background: -webkit-gradient(linear, left top, right top, color-stop(25%, #f2f2f2), color-stop(37%, #e6e6e6), color-stop(63%, #f2f2f2));
|
||||
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
|
||||
background-size: 400% 100%;
|
||||
-webkit-animation: el-skeleton-loading 1.4s ease infinite;
|
||||
animation: el-skeleton-loading 1.4s ease infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes el-skeleton-loading {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes el-skeleton-loading {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
|
@ -31,7 +31,8 @@
|
|||
"timeago.js": "^4.0.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-fragment": "^1.5.2",
|
||||
"vue-i18n": "^8.26.7"
|
||||
"vue-i18n": "^8.26.7",
|
||||
"xss": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
|
@ -62,7 +63,7 @@
|
|||
"babel-eslint": "^10.0.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"dateformat": "^3.0.3",
|
||||
"element-ui": "~2.13.0",
|
||||
"element-ui": "~2.15.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-vue": "^7.16.0",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"typescript": "~4.3.5",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-agile": "^2.0.0",
|
||||
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
|
||||
"vue-json-pretty": "1.7.1",
|
||||
"vue-prism-editor": "^0.3.0",
|
||||
|
|
|
@ -1,46 +1,125 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<div id="header">
|
||||
<router-view name="header"></router-view>
|
||||
<div>
|
||||
<LoadingView v-if="loading" />
|
||||
<div v-else id="app">
|
||||
<div id="header">
|
||||
<router-view name="header"></router-view>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<router-view name="sidebar"></router-view>
|
||||
</div>
|
||||
<div id="content">
|
||||
<router-view />
|
||||
</div>
|
||||
<Modals />
|
||||
<Telemetry />
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<router-view name="sidebar"></router-view>
|
||||
</div>
|
||||
<div id="content">
|
||||
<router-view />
|
||||
</div>
|
||||
<Telemetry />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Telemetry from './components/Telemetry.vue';
|
||||
import { HIRING_BANNER } from './constants';
|
||||
import Modals from '@/components/Modals.vue';
|
||||
import LoadingView from './views/LoadingView.vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from './components/mixins/showMessage';
|
||||
|
||||
export default Vue.extend({
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'App',
|
||||
components: {
|
||||
LoadingView,
|
||||
Modals,
|
||||
Telemetry,
|
||||
},
|
||||
mounted() {
|
||||
this.$telemetry.page('Editor', this.$route.name);
|
||||
computed: {
|
||||
...mapGetters('settings', ['isInternalUser', 'isTemplatesEnabled', 'isTemplatesEndpointReachable']),
|
||||
isRootPath(): boolean {
|
||||
return this.$route.path === '/';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async initSettings(): Promise<void> {
|
||||
try {
|
||||
await this.$store.dispatch('settings/getSettings');
|
||||
} catch (e) {
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('settings.errors.connectionError.title'),
|
||||
message: this.$locale.baseText('settings.errors.connectionError.message'),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async initTemplates(): Promise<void> {
|
||||
try {
|
||||
const templatesPromise = this.$store.dispatch('settings/testTemplatesEndpoint');
|
||||
if (this.isRootPath) { // only delay loading to determine redirect
|
||||
await templatesPromise;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
},
|
||||
async initialize(): Promise<void> {
|
||||
await this.initSettings();
|
||||
await this.initTemplates();
|
||||
|
||||
if (!this.isInternalUser && this.$route.name !== 'WorkflowDemo') {
|
||||
console.log(HIRING_BANNER); // eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
trackPage() {
|
||||
this.$store.commit('ui/setCurrentView', this.$route.name);
|
||||
if (this.$route && this.$route.meta && this.$route.meta.templatesEnabled) {
|
||||
this.$store.commit('templates/setSessionId');
|
||||
}
|
||||
else {
|
||||
this.$store.commit('templates/resetSessionId'); // reset telemetry session id when user leaves template pages
|
||||
}
|
||||
|
||||
this.$telemetry.page('Editor', this.$route);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.initialize();
|
||||
|
||||
if (this.isTemplatesEnabled && this.isTemplatesEndpointReachable && this.isRootPath) {
|
||||
this.$router.replace({ name: 'TemplatesSearchView'});
|
||||
} else if (this.isRootPath) {
|
||||
this.$router.replace({ name: 'NodeViewNew'});
|
||||
}
|
||||
else if (!this.isTemplatesEnabled && this.$route.meta && this.$route.meta.templatesEnabled) {
|
||||
this.$router.replace({ name: 'NodeViewNew'});
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
this.trackPage();
|
||||
this.$externalHooks().run('app.mount');
|
||||
},
|
||||
watch: {
|
||||
'$route'(route) {
|
||||
this.$telemetry.page('Editor', route.name);
|
||||
'$route'() {
|
||||
this.trackPage();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
#app {
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#content {
|
||||
background-color: var(--color-background-light);
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
|
@ -253,7 +253,7 @@ export interface IWorkflowDataUpdate {
|
|||
}
|
||||
|
||||
export interface IWorkflowTemplate {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
workflow: {
|
||||
nodes: INodeUi[];
|
||||
|
@ -514,6 +514,64 @@ export interface IN8nPromptResponse {
|
|||
updated: boolean;
|
||||
}
|
||||
|
||||
export interface ITemplatesCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
nodes: ITemplatesNode[];
|
||||
workflows: Array<{id: number}>;
|
||||
}
|
||||
|
||||
interface ITemplatesImage {
|
||||
id: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ITemplatesCollectionExtended extends ITemplatesCollection {
|
||||
description: string | null;
|
||||
image: ITemplatesImage[];
|
||||
categories: ITemplatesCategory[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ITemplatesCollectionFull extends ITemplatesCollectionExtended {
|
||||
full: true;
|
||||
}
|
||||
|
||||
export interface ITemplatesCollectionResponse extends ITemplatesCollectionExtended {
|
||||
workflows: ITemplatesWorkflow[];
|
||||
}
|
||||
|
||||
export interface ITemplatesWorkflow {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
nodes: ITemplatesNode[];
|
||||
totalViews: number;
|
||||
user: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITemplatesWorkflowResponse extends ITemplatesWorkflow, IWorkflowTemplate {
|
||||
description: string | null;
|
||||
image: ITemplatesImage[];
|
||||
categories: ITemplatesCategory[];
|
||||
}
|
||||
|
||||
export interface ITemplatesWorkflowFull extends ITemplatesWorkflowResponse {
|
||||
full: true;
|
||||
}
|
||||
|
||||
export interface ITemplatesQuery {
|
||||
categories: number[];
|
||||
search: string;
|
||||
}
|
||||
|
||||
export interface ITemplatesCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IN8nUISettings {
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
|
@ -538,6 +596,11 @@ export interface IN8nUISettings {
|
|||
telemetry: ITelemetrySettings;
|
||||
defaultLocale: string;
|
||||
logLevel: ILogLevel;
|
||||
deploymentType: string;
|
||||
templates: {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
@ -644,7 +707,13 @@ export interface IVersionNode {
|
|||
icon?: string;
|
||||
fileBuffer?: string;
|
||||
};
|
||||
typeVersion?: number;
|
||||
}
|
||||
|
||||
export interface ITemplatesNode extends IVersionNode {
|
||||
categories?: ITemplatesCategory[];
|
||||
}
|
||||
|
||||
export interface IRootState {
|
||||
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
||||
activeWorkflows: string[];
|
||||
|
@ -717,6 +786,7 @@ export interface IUiState {
|
|||
[key: string]: IModalState;
|
||||
};
|
||||
isPageLoading: boolean;
|
||||
currentView: string;
|
||||
}
|
||||
|
||||
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
||||
|
@ -724,6 +794,27 @@ export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
|||
export interface ISettingsState {
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
templatesEndpointHealthy: boolean;
|
||||
}
|
||||
|
||||
export interface ITemplateState {
|
||||
categories: {[id: string]: ITemplatesCategory};
|
||||
collections: {[id: string]: ITemplatesCollection};
|
||||
workflows: {[id: string]: ITemplatesWorkflow};
|
||||
workflowSearches: {
|
||||
[search: string]: {
|
||||
workflowIds: string[];
|
||||
totalWorkflows: number;
|
||||
loadingMore?: boolean;
|
||||
}
|
||||
};
|
||||
collectionSearches: {
|
||||
[search: string]: {
|
||||
collectionIds: string[];
|
||||
}
|
||||
};
|
||||
currentSessionId: string;
|
||||
previousSessionId: string;
|
||||
}
|
||||
|
||||
export interface IVersionsState {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IDataObject } from 'n8n-workflow';
|
||||
import { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
||||
import { makeRestApiRequest, get, post } from './helpers';
|
||||
import { TEMPLATES_BASE_URL } from '@/constants';
|
||||
import { N8N_IO_BASE_URL } from '@/constants';
|
||||
|
||||
export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
|
||||
return await makeRestApiRequest(context, 'GET', '/settings');
|
||||
|
@ -12,14 +12,14 @@ export async function submitPersonalizationSurvey(context: IRestApiContext, para
|
|||
}
|
||||
|
||||
export async function getPromptsData(instanceId: string): Promise<IN8nPrompts> {
|
||||
return await get(TEMPLATES_BASE_URL, '/prompts', {}, {'n8n-instance-id': instanceId});
|
||||
return await get(N8N_IO_BASE_URL, '/prompts', {}, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
export async function submitContactInfo(instanceId: string, email: string): Promise<void> {
|
||||
return await post(TEMPLATES_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId});
|
||||
return await post(N8N_IO_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
export async function submitValueSurvey(instanceId: string, params: IN8nValueSurveyData): Promise<IN8nPrompts> {
|
||||
return await post(TEMPLATES_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId});
|
||||
return await post(N8N_IO_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
|
|
39
packages/editor-ui/src/api/templates.ts
Normal file
39
packages/editor-ui/src/api/templates.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { ITemplatesCategory, ITemplatesCollection, ITemplatesQuery, ITemplatesWorkflow, ITemplatesCollectionResponse, ITemplatesWorkflowResponse, IWorkflowTemplate } from '@/Interface';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { get } from './helpers';
|
||||
|
||||
function stringifyArray(arr: number[]) {
|
||||
return arr.join(',');
|
||||
}
|
||||
|
||||
export function testHealthEndpoint(apiEndpoint: string) {
|
||||
return get(apiEndpoint, '/health');
|
||||
}
|
||||
|
||||
export function getCategories(apiEndpoint: string, headers?: IDataObject): Promise<{categories: ITemplatesCategory[]}> {
|
||||
return get(apiEndpoint, '/templates/categories', undefined, headers);
|
||||
}
|
||||
|
||||
export async function getCollections(apiEndpoint: string, query: ITemplatesQuery, headers?: IDataObject): Promise<{collections: ITemplatesCollection[]}> {
|
||||
return await get(apiEndpoint, '/templates/collections', {category: stringifyArray(query.categories || []), search: query.search}, headers);
|
||||
}
|
||||
|
||||
export async function getWorkflows(
|
||||
apiEndpoint: string,
|
||||
query: {skip: number, limit: number, categories: number[], search: string},
|
||||
headers?: IDataObject,
|
||||
): Promise<{totalWorkflows: number, workflows: ITemplatesWorkflow[]}> {
|
||||
return get(apiEndpoint, '/templates/workflows', {skip: query.skip, rows: query.limit, category: stringifyArray(query.categories), search: query.search}, headers);
|
||||
}
|
||||
|
||||
export async function getCollectionById(apiEndpoint: string, collectionId: string, headers?: IDataObject): Promise<{collection: ITemplatesCollectionResponse}> {
|
||||
return await get(apiEndpoint, `/templates/collections/${collectionId}`, undefined, headers);
|
||||
}
|
||||
|
||||
export async function getTemplateById(apiEndpoint: string, templateId: string, headers?: IDataObject): Promise<{workflow: ITemplatesWorkflowResponse}> {
|
||||
return await get(apiEndpoint, `/templates/workflows/${templateId}`, undefined, headers);
|
||||
}
|
||||
|
||||
export async function getWorkflowTemplate(apiEndpoint: string, templateId: string, headers?: IDataObject): Promise<IWorkflowTemplate> {
|
||||
return await get(apiEndpoint, `/workflows/templates/${templateId}`, undefined, headers);
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
import { IRestApiContext, IWorkflowTemplate } from '@/Interface';
|
||||
import { makeRestApiRequest, get } from './helpers';
|
||||
import { TEMPLATES_BASE_URL } from '@/constants';
|
||||
import { IRestApiContext } from '@/Interface';
|
||||
import { makeRestApiRequest } from './helpers';
|
||||
|
||||
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
||||
return await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
|
||||
}
|
||||
|
||||
export async function getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
|
||||
return await get(TEMPLATES_BASE_URL, `/workflows/templates/${templateId}`);
|
||||
}
|
||||
|
|
80
packages/editor-ui/src/components/Card.vue
Normal file
80
packages/editor-ui/src/components/Card.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div
|
||||
:class="$style.card"
|
||||
@click="(e) => $emit('click', e)"
|
||||
>
|
||||
<div :class="$style.container">
|
||||
<span
|
||||
v-if="!loading"
|
||||
v-text="title"
|
||||
:class="$style.title"
|
||||
/>
|
||||
<n8n-loading :loading="loading" :rows="3" variant="p" />
|
||||
<div :class="$style.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'Card',
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.card {
|
||||
width: 240px !important;
|
||||
height: 140px;
|
||||
border-radius: var(--border-radius-large);
|
||||
border: $--version-card-border;
|
||||
margin-right: var(--spacing-2xs);
|
||||
background-color: var(--color-background-xlight);
|
||||
padding: var(--spacing-s);
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(68,28,23,0.07);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: var(--font-size-s);
|
||||
line-height: var(--font-line-height-regular);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
47
packages/editor-ui/src/components/CollectionCard.vue
Normal file
47
packages/editor-ui/src/components/CollectionCard.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<Card
|
||||
:loading="loading"
|
||||
:title="collection.name"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-slot:footer>
|
||||
<n8n-text size="small" color="text-light">
|
||||
{{ collection.workflows.length }}
|
||||
{{ $locale.baseText('templates.workflows') }}
|
||||
</n8n-text>
|
||||
<NodeList :nodes="collection.nodes" :showMore="false" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import Card from '@/components/Card.vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import NodeList from '@/components/NodeList.vue';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'CollectionCard',
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
collection: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Card,
|
||||
NodeList,
|
||||
},
|
||||
methods: {
|
||||
onClick(e: MouseEvent) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
181
packages/editor-ui/src/components/CollectionsCarousel.vue
Normal file
181
packages/editor-ui/src/components/CollectionsCarousel.vue
Normal file
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<div :class="$style.container" v-show="loading || collections.length">
|
||||
<agile ref="slider" :dots="false" :navButtons="false" :infinite="false" :slides-to-show="4" @after-change="updateCarouselScroll">
|
||||
<Card v-for="n in (loading ? 4: 0)" :key="`loading-${n}`" :loading="loading" />
|
||||
<CollectionCard
|
||||
v-for="collection in (loading? []: collections)"
|
||||
:key="collection.id"
|
||||
:collection="collection"
|
||||
@click="(e) => onCardClick(e, collection.id)"
|
||||
/>
|
||||
</agile>
|
||||
<button v-show="carouselScrollPosition > 0" :class="$style.leftButton" @click="scrollLeft">
|
||||
<font-awesome-icon icon="chevron-left" />
|
||||
</button>
|
||||
<button v-show="!scrollEnd" :class="$style.rightButton" @click="scrollRight">
|
||||
<font-awesome-icon icon="chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Card from '@/components/Card.vue';
|
||||
import CollectionCard from '@/components/CollectionCard.vue';
|
||||
import VueAgile from 'vue-agile';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'CollectionsCarousel',
|
||||
props: {
|
||||
collections: {
|
||||
type: Array,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
collections() {
|
||||
setTimeout(() => {
|
||||
this.updateCarouselScroll();
|
||||
}, 0);
|
||||
},
|
||||
loading() {
|
||||
setTimeout(() => {
|
||||
this.updateCarouselScroll();
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Card,
|
||||
CollectionCard,
|
||||
VueAgile,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
carouselScrollPosition: 0,
|
||||
cardWidth: 240,
|
||||
scrollEnd: false,
|
||||
listElement: null as null | Element,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateCarouselScroll() {
|
||||
if (this.listElement) {
|
||||
this.carouselScrollPosition = Number(this.listElement.scrollLeft.toFixed());
|
||||
|
||||
const width = this.listElement.clientWidth;
|
||||
const scrollWidth = this.listElement.scrollWidth;
|
||||
const scrollLeft = this.carouselScrollPosition;
|
||||
this.scrollEnd = scrollWidth - width <= scrollLeft + 7;
|
||||
}
|
||||
},
|
||||
onCardClick(event: MouseEvent, id: string) {
|
||||
this.$emit('openCollection', {event, id});
|
||||
},
|
||||
scrollLeft() {
|
||||
if (this.listElement) {
|
||||
this.listElement.scrollBy({ left: -(this.cardWidth * 2), top: 0, behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
scrollRight() {
|
||||
if (this.listElement) {
|
||||
this.listElement.scrollBy({ left: this.cardWidth * 2, top: 0, behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const slider = this.$refs.slider;
|
||||
if (!slider) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
this.listElement = slider.$el.querySelector('.agile__list');
|
||||
if (this.listElement) {
|
||||
this.listElement.addEventListener('scroll', this.updateCarouselScroll);
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$refs.slider) {
|
||||
// @ts-ignore
|
||||
this.$refs.slider.destroy();
|
||||
}
|
||||
window.removeEventListener('scroll', this.updateCarouselScroll);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 28px;
|
||||
height: 37px;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
border-radius: var(--border-radius-large);
|
||||
border: var(--border-base);
|
||||
background-color: #fbfcfe;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: 40px;
|
||||
height: 140px;
|
||||
top: -55px;
|
||||
position: absolute;
|
||||
}
|
||||
svg {
|
||||
color: var(--color-foreground-xdark);
|
||||
}
|
||||
}
|
||||
|
||||
.leftButton {
|
||||
composes: button;
|
||||
left: -30px;
|
||||
|
||||
&:after {
|
||||
left: 27px;
|
||||
background: linear-gradient(270deg, rgba(255, 255, 255, 0.25) 0%, rgba(248, 249, 251, 1) 86%);
|
||||
}
|
||||
}
|
||||
|
||||
.rightButton {
|
||||
composes: button;
|
||||
right: -30px;
|
||||
&:after {
|
||||
right: 27px;
|
||||
background: linear-gradient(270deg,rgba(248, 249, 251, 1) 25%, rgba(255, 255, 255, 0.25) 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.agile {
|
||||
&__list {
|
||||
width: 100%;
|
||||
padding-bottom: var(--spacing-2xs);
|
||||
overflow-x: scroll;
|
||||
transition: all 1s ease-in-out;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-foreground-dark);
|
||||
}
|
||||
}
|
||||
|
||||
&__track {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -16,7 +16,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="credentialsToDisplay" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
|
||||
<el-table :data="credentialsToDisplay" v-loading="loading" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
|
||||
<el-table-column property="name" :label="$locale.baseText('credentialsList.name')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" :label="$locale.baseText('credentialsList.type')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" :label="$locale.baseText('credentialsList.created')" class-name="clickable" sortable></el-table-column>
|
||||
|
@ -64,11 +64,12 @@ export default mixins(
|
|||
data() {
|
||||
return {
|
||||
CREDENTIAL_LIST_MODAL_KEY,
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentials']),
|
||||
credentialsToDisplay() {
|
||||
credentialsToDisplay(): ICredentialsResponse[] {
|
||||
return this.allCredentials.reduce((accu: ICredentialsResponse[], cred: ICredentialsResponse) => {
|
||||
const type = this.$store.getters['credentials/getCredentialTypeByName'](cred.type);
|
||||
|
||||
|
@ -85,7 +86,17 @@ export default mixins(
|
|||
}, []);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
try {
|
||||
await Promise.all([
|
||||
await this.$store.dispatch('credentials/fetchCredentialTypes'),
|
||||
await this.$store.dispatch('credentials/fetchAllCredentials'),
|
||||
]);
|
||||
} catch (e) {
|
||||
this.$showError(e, this.$locale.baseText('credentialsList.errorLoadingCredentials'));
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
this.$externalHooks().run('credentialsList.mounted');
|
||||
this.$telemetry.track('User opened Credentials panel', { workflow_id: this.$store.getters.workflowId });
|
||||
},
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
:eventBus="modalBus"
|
||||
width="50%"
|
||||
:center="true"
|
||||
:loading="loading"
|
||||
maxWidth="460px"
|
||||
minHeight="250px"
|
||||
>
|
||||
<template slot="header">
|
||||
<h2 :class="$style.title">{{ $locale.baseText('credentialSelectModal.addNewCredential') }}</h2>
|
||||
|
@ -58,7 +60,13 @@ export default Vue.extend({
|
|||
components: {
|
||||
Modal,
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
try {
|
||||
await this.$store.dispatch('credentials/fetchCredentialTypes');
|
||||
} catch (e) {
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
setTimeout(() => {
|
||||
const element = this.$refs.select as HTMLSelectElement;
|
||||
if (element) {
|
||||
|
@ -70,6 +78,7 @@ export default Vue.extend({
|
|||
return {
|
||||
modalBus: new Vue(),
|
||||
selected: '',
|
||||
loading: true,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
};
|
||||
},
|
||||
|
|
55
packages/editor-ui/src/components/GoBackButton.vue
Normal file
55
packages/editor-ui/src/components/GoBackButton.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div :class="$style.wrapper" @click="navigateTo">
|
||||
<font-awesome-icon :class="$style.icon" icon="arrow-left" />
|
||||
<div :class="$style.text" v-text="$locale.baseText('template.buttons.goBackButton')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TemplateList',
|
||||
data() {
|
||||
return {
|
||||
routeHasHistory: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
navigateTo() {
|
||||
if (this.routeHasHistory) this.$router.go(-1);
|
||||
else this.$router.push({ name: 'TemplatesSearchView' });
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.history.state ? this.routeHasHistory = true : this.routeHasHistory = false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.icon,
|
||||
.text {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-2xs);
|
||||
color: var(--color-foreground-dark);
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: var(--font-size-s);
|
||||
line-height: var(--font-line-height-loose);
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
</style>
|
176
packages/editor-ui/src/components/HoverableNodeIcon.vue
Normal file
176
packages/editor-ui/src/components/HoverableNodeIcon.vue
Normal file
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div
|
||||
:class="$style.wrapper"
|
||||
:style="iconStyleData"
|
||||
@click="(e) => $emit('click')"
|
||||
@mouseover="showTooltip = true"
|
||||
@mouseleave="showTooltip = false"
|
||||
>
|
||||
<div :class="$style.tooltip">
|
||||
<n8n-tooltip placement="top" :manual="true" :value="showTooltip">
|
||||
<div slot="content" v-text="nodeType.displayName"></div>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div v-if="nodeIconData !== null" :class="$style.icon" title="">
|
||||
<div :class="$style.iconWrapper" :style="iconStyleData">
|
||||
<div v-if="nodeIconData !== null" :class="$style.icon">
|
||||
<img
|
||||
v-if="nodeIconData.type === 'file'"
|
||||
:src="nodeIconData.fileBuffer || nodeIconData.path"
|
||||
:style="imageStyleData"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
:icon="nodeIconData.icon || nodeIconData.path"
|
||||
:style="fontStyleData"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="node-icon-placeholder">
|
||||
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.placeholder">
|
||||
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { ITemplatesNode } from '@/Interface';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
interface NodeIconData {
|
||||
type: string;
|
||||
path?: string;
|
||||
fileExtension?: string;
|
||||
fileBuffer?: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'HoverableNodeIcon',
|
||||
props: {
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clickButton: {
|
||||
type: Function,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodeType: {
|
||||
type: Object,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fontStyleData(): object {
|
||||
return {
|
||||
'max-width': this.size + 'px',
|
||||
};
|
||||
},
|
||||
iconStyleData(): object {
|
||||
const nodeType = this.nodeType as ITemplatesNode | null;
|
||||
const color = nodeType ? nodeType.defaults && nodeType!.defaults.color : '';
|
||||
if (!this.size) {
|
||||
return { color };
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
width: this.size + 'px',
|
||||
height: this.size + 'px',
|
||||
'font-size': this.size + 'px',
|
||||
'line-height': this.size + 'px',
|
||||
'border-radius': this.circle ? '50%' : '2px',
|
||||
...(this.disabled && {
|
||||
color: '#ccc',
|
||||
'-webkit-filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
filter: 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
}),
|
||||
};
|
||||
},
|
||||
imageStyleData(): object {
|
||||
return {
|
||||
width: '100%',
|
||||
'max-width': '100%',
|
||||
'max-height': '100%',
|
||||
};
|
||||
},
|
||||
nodeIconData(): null | NodeIconData {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | ITemplatesNode | null;
|
||||
if (nodeType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((nodeType as ITemplatesNode).iconData) {
|
||||
return (nodeType as ITemplatesNode).iconData;
|
||||
}
|
||||
|
||||
const restUrl = this.$store.getters.getRestUrl;
|
||||
|
||||
if (nodeType.icon) {
|
||||
let type, path;
|
||||
[type, path] = nodeType.icon.split(':');
|
||||
const returnData: NodeIconData = {
|
||||
type,
|
||||
path,
|
||||
};
|
||||
|
||||
if (type === 'file') {
|
||||
returnData.path = restUrl + '/node-icon/' + nodeType.name;
|
||||
returnData.fileExtension = path.split('.').slice(-1).join();
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
cursor: pointer;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
left: 10px;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
|
@ -30,49 +30,55 @@
|
|||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item v-if="isTemplatesEnabled" index="template-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="box-open"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.newTemplate') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-save">
|
||||
<n8n-menu-item index="workflow-save" :disabled="!onWorkflowPage">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="save"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.save') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
<n8n-menu-item index="workflow-duplicate" :disabled="!onWorkflowPage || !currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="copy"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.duplicate') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
<n8n-menu-item index="workflow-delete" :disabled="!onWorkflowPage || !currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.delete') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-download">
|
||||
<n8n-menu-item index="workflow-download" :disabled="!onWorkflowPage">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file-download"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.download') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-url">
|
||||
<n8n-menu-item index="workflow-import-url" :disabled="!onWorkflowPage">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cloud"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromUrl') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-file">
|
||||
<n8n-menu-item index="workflow-import-file" :disabled="!onWorkflowPage">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="hdd"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromFile') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-settings" :disabled="!currentWorkflow">
|
||||
<n8n-menu-item index="workflow-settings" :disabled="!onWorkflowPage || !currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cog"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.settings') }}</span>
|
||||
|
@ -80,6 +86,11 @@
|
|||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<n8n-menu-item v-if="isTemplatesEnabled" index="templates">
|
||||
<font-awesome-icon icon="box-open"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.templates') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<el-submenu index="credentials" :title="$locale.baseText('mainSidebar.credentials')" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="key"/>
|
||||
|
@ -165,7 +176,19 @@ import { saveAs } from 'file-saver';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
||||
import { CREDENTIAL_LIST_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, EXECUTIONS_MODAL_KEY } from '@/constants';
|
||||
import {
|
||||
CREDENTIAL_LIST_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
MODAL_CANCEL,
|
||||
MODAL_CLOSE,
|
||||
MODAL_CONFIRMED,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_OPEN_MODAL_KEY,
|
||||
EXECUTIONS_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
|
@ -200,6 +223,9 @@ export default mixins(
|
|||
'hasVersionUpdates',
|
||||
'nextVersions',
|
||||
]),
|
||||
...mapGetters('settings', [
|
||||
'isTemplatesEnabled',
|
||||
]),
|
||||
helpMenuItems (): object[] {
|
||||
return [
|
||||
{
|
||||
|
@ -286,6 +312,9 @@ export default mixins(
|
|||
sidebarMenuBottomItems(): IMenuItem[] {
|
||||
return this.$store.getters.sidebarMenuItems.filter((item: IMenuItem) => item.position === 'bottom');
|
||||
},
|
||||
onWorkflowPage(): boolean {
|
||||
return this.$route.meta && this.$route.meta.nodeView;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trackHelpItemClick (itemType: string) {
|
||||
|
@ -449,14 +478,30 @@ export default mixins(
|
|||
} else if (key === 'workflow-new') {
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(
|
||||
const confirmModal = await this.confirmModal(
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.message'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.cancelButtonText'),
|
||||
true,
|
||||
);
|
||||
if (importConfirm === true) {
|
||||
|
||||
if (confirmModal === MODAL_CONFIRMED) {
|
||||
const saved = await this.saveCurrentWorkflow({}, false);
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
|
||||
if (this.$router.currentRoute.name === 'NodeViewNew') {
|
||||
this.$root.$emit('newWorkflow');
|
||||
} else {
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
|
||||
type: 'success',
|
||||
});
|
||||
} else if (confirmModal === MODAL_CANCEL) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
if (this.$router.currentRoute.name === 'NodeViewNew') {
|
||||
this.$root.$emit('newWorkflow');
|
||||
|
@ -468,6 +513,8 @@ export default mixins(
|
|||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
|
||||
type: 'success',
|
||||
});
|
||||
} else if (confirmModal === MODAL_CLOSE) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this.$router.currentRoute.name !== 'NodeViewNew') {
|
||||
|
@ -480,6 +527,10 @@ export default mixins(
|
|||
});
|
||||
}
|
||||
this.$titleReset();
|
||||
} else if (key === 'templates' || key === 'template-new') {
|
||||
if (this.$router.currentRoute.name !== 'TemplatesSearchView') {
|
||||
this.$router.push({ name: 'TemplatesSearchView' });
|
||||
}
|
||||
} else if (key === 'credentials-open') {
|
||||
this.$store.dispatch('ui/openModal', CREDENTIAL_LIST_MODAL_KEY);
|
||||
} else if (key === 'credentials-new') {
|
||||
|
@ -554,7 +605,8 @@ export default mixins(
|
|||
}
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 73px;
|
||||
left: 56px;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
.item-title-root {
|
||||
position: absolute;
|
||||
|
@ -563,6 +615,12 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
.el-menu--inline {
|
||||
.el-menu-item {
|
||||
padding-left: 30px!important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
|
|
|
@ -96,8 +96,8 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
nodeTypes(newList, prevList) {
|
||||
if (prevList.length === 0) {
|
||||
nodeTypes(newList) {
|
||||
if (newList.length !== this.allNodeTypes.length) {
|
||||
this.allNodeTypes = newList;
|
||||
}
|
||||
},
|
||||
|
|
111
packages/editor-ui/src/components/NodeList.vue
Normal file
111
packages/editor-ui/src/components/NodeList.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div :class="$style.list">
|
||||
<div v-for="node in slicedNodes" :class="[$style.container, $style[size]]" :key="node.name">
|
||||
<HoverableNodeIcon :nodeType="node" :size="size === 'md'? 24: 18" :title="node.name" />
|
||||
</div>
|
||||
<div :class="[$style.button, size === 'md' ? $style.buttonMd : $style.buttonSm]" v-if="filteredCoreNodes.length > limit + 1">
|
||||
+{{ hiddenNodes }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HoverableNodeIcon from '@/components/HoverableNodeIcon.vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { ITemplatesNode } from '@/Interface';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { filterTemplateNodes } from './helpers';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'NodeList',
|
||||
props: {
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
HoverableNodeIcon,
|
||||
},
|
||||
computed: {
|
||||
filteredCoreNodes() {
|
||||
return filterTemplateNodes(this.nodes as ITemplatesNode[]);
|
||||
},
|
||||
hiddenNodes(): number {
|
||||
return this.filteredCoreNodes.length - this.countNodesToBeSliced(this.filteredCoreNodes);
|
||||
},
|
||||
slicedNodes(): ITemplatesNode[] {
|
||||
return this.filteredCoreNodes.slice(0, this.countNodesToBeSliced(this.filteredCoreNodes));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
countNodesToBeSliced(nodes: ITemplatesNode[]): number {
|
||||
if (nodes.length > this.limit) {
|
||||
return this.limit - 1;
|
||||
} else {
|
||||
return this.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.list {
|
||||
max-width: 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sm {
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.md {
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.button {
|
||||
top: 0px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-light);
|
||||
border: 1px var(--color-foreground-base) solid;
|
||||
border-radius: var(--border-radius-base);
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.buttonSm {
|
||||
margin-left: var(--spacing-2xs);
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.buttonMd {
|
||||
margin-left: var(--spacing-xs);
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
|
@ -143,11 +143,12 @@
|
|||
:value="displayValue"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
:placeholder="$locale.baseText('parameterInput.select')"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
>
|
||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="getOptionsOptionDisplayName(option)">
|
||||
<div class="list-option">
|
||||
|
@ -302,6 +303,9 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
areExpressionsDisabled(): boolean {
|
||||
return this.$store.getters['ui/areExpressionsDisabled'];
|
||||
},
|
||||
codeAutocomplete (): string | undefined {
|
||||
return this.getArgument('codeAutocomplete') as string | undefined;
|
||||
},
|
||||
|
@ -419,6 +423,10 @@ export default mixins(
|
|||
return false;
|
||||
},
|
||||
expressionValueComputed (): NodeParameterValue | null {
|
||||
if (this.areExpressionsDisabled) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
if (this.node === null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -661,6 +669,10 @@ export default mixins(
|
|||
this.valueChanged(value);
|
||||
},
|
||||
openExpressionEdit() {
|
||||
if (this.areExpressionsDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isValueExpression) {
|
||||
this.expressionEditDialogVisible = true;
|
||||
this.trackExpressionEditOpen();
|
||||
|
|
|
@ -9,15 +9,43 @@ import { mapGetters } from 'vuex';
|
|||
|
||||
export default Vue.extend({
|
||||
name: 'Telemetry',
|
||||
data() {
|
||||
return {
|
||||
initialised: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('settings', ['telemetry']),
|
||||
isTelemeteryEnabledOnRoute(): boolean {
|
||||
return this.$route.meta && this.$route.meta.telemetry ? !this.$route.meta.telemetry.disabled: true;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (this.initialised || !this.isTelemeteryEnabledOnRoute) {
|
||||
return;
|
||||
}
|
||||
const opts = this.telemetry;
|
||||
if (opts && opts.enabled) {
|
||||
this.initialised = true;
|
||||
const instanceId = this.$store.getters.instanceId;
|
||||
const logLevel = this.$store.getters['settings/logLevel'];
|
||||
this.$telemetry.init(opts, {instanceId, logLevel, store: this.$store});
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
telemetry(opts) {
|
||||
if (opts && opts.enabled) {
|
||||
this.$telemetry.init(opts, this.$store.getters.instanceId, this.$store.getters['settings/logLevel']);
|
||||
isTelemeteryEnabledOnRoute(enabled) {
|
||||
if (enabled) {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
telemetry() {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
176
packages/editor-ui/src/components/TemplateCard.vue
Normal file
176
packages/editor-ui/src/components/TemplateCard.vue
Normal file
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[$style.card, lastItem && $style.last, firstItem && $style.first, !loading && $style.loaded]"
|
||||
@click="onCardClick"
|
||||
>
|
||||
<div :class="$style.loading" v-if="loading">
|
||||
<n8n-loading :rows="2" :shrinkLast="false" :loading="loading" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<n8n-heading :bold="true" size="small">{{ workflow.name }}</n8n-heading>
|
||||
<div :class="$style.content">
|
||||
<span v-if="workflow.totalViews">
|
||||
<n8n-text size="small" color="text-light">
|
||||
<font-awesome-icon icon="eye" />
|
||||
{{ abbreviateNumber(workflow.totalViews) }}
|
||||
</n8n-text>
|
||||
</span>
|
||||
<div v-if="workflow.totalViews" :class="$style.line" v-text="'|'" />
|
||||
<n8n-text size="small" color="text-light">
|
||||
<TimeAgo :date="workflow.createdAt" />
|
||||
</n8n-text>
|
||||
<div v-if="workflow.user" :class="$style.line" v-text="'|'" />
|
||||
<n8n-text v-if="workflow.user" size="small" color="text-light">By {{ workflow.user.username }}</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.nodesContainer, useWorkflowButton && $style.hideOnHover]" v-if="!loading">
|
||||
<NodeList v-if="workflow.nodes" :nodes="workflow.nodes" :limit="nodesToBeShown" size="md" />
|
||||
</div>
|
||||
<div :class="$style.buttonContainer" v-if="useWorkflowButton">
|
||||
<n8n-button
|
||||
v-if="useWorkflowButton"
|
||||
type="outline"
|
||||
label="Use workflow"
|
||||
@click.stop="onUseWorkflowClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { filterTemplateNodes, abbreviateNumber } from './helpers';
|
||||
import NodeList from './NodeList.vue';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'TemplateCard',
|
||||
props: {
|
||||
lastItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
firstItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
workflow: {
|
||||
type: Object,
|
||||
},
|
||||
useWorkflowButton: {
|
||||
type: Boolean,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
NodeList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodesToBeShown: 5,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
filterTemplateNodes,
|
||||
abbreviateNumber,
|
||||
countNodesToBeSliced(nodes: []): number {
|
||||
if (nodes.length > this.nodesToBeShown) {
|
||||
return this.nodesToBeShown - 1;
|
||||
} else {
|
||||
return this.nodesToBeShown;
|
||||
}
|
||||
},
|
||||
onUseWorkflowClick(e: MouseEvent) {
|
||||
this.$emit('useWorkflow', e);
|
||||
},
|
||||
onCardClick(e: MouseEvent) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.nodes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
border-left: var(--border-base);
|
||||
border-right: var(--border-base);
|
||||
border-bottom: var(--border-base);
|
||||
background-color: var(--color-background-xlight);
|
||||
|
||||
display: flex;
|
||||
padding: 0 var(--spacing-s) var(--spacing-s) var(--spacing-s);
|
||||
background-color: var(--color-background-xlight);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.hideOnHover {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
.loaded {
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.first {
|
||||
border-top: var(--border-base);
|
||||
border-top-right-radius: var(--border-radius-large);
|
||||
border-top-left-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.last {
|
||||
border-bottom-right-radius: var(--border-radius-large);
|
||||
border-bottom-left-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.line {
|
||||
padding: 0 6px;
|
||||
color: var(--color-foreground-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
background-color: var(--color-background-xlight);
|
||||
}
|
||||
|
||||
.nodesContainer {
|
||||
min-width: 175px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
</style>
|
103
packages/editor-ui/src/components/TemplateDetails.vue
Normal file
103
packages/editor-ui/src/components/TemplateDetails.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div>
|
||||
<n8n-loading :loading="loading" :rows="5" variant="p" />
|
||||
|
||||
<template-details-block v-if="!loading && template.nodes.length > 0" :title="blockTitle">
|
||||
<div :class="$style.icons">
|
||||
<div
|
||||
v-for="node in filterTemplateNodes(template.nodes)"
|
||||
:key="node.name"
|
||||
:class="$style.icon"
|
||||
>
|
||||
<HoverableNodeIcon
|
||||
:nodeType="node"
|
||||
:title="node.name"
|
||||
:size="24"
|
||||
@click="redirectToSearchPage(node)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template-details-block>
|
||||
|
||||
<template-details-block
|
||||
v-if="!loading && template.categories.length > 0"
|
||||
:title="$locale.baseText('template.details.categories')"
|
||||
>
|
||||
<n8n-tags :tags="template.categories" @click="redirectToCategory" />
|
||||
</template-details-block>
|
||||
|
||||
<template-details-block v-if="!loading" :title="$locale.baseText('template.details.details')">
|
||||
<div :class="$style.text">
|
||||
<n8n-text size="small" color="text-base">
|
||||
{{ $locale.baseText('template.details.created') }}
|
||||
<TimeAgo :date="template.createdAt" />
|
||||
<span>{{ $locale.baseText('template.details.by') }}</span>
|
||||
<span v-if="template.user"> {{ template.user.username }}</span>
|
||||
<span v-else> n8n team</span>
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div :class="$style.text">
|
||||
<n8n-text v-if="template.totalViews !== 0" size="small" color="text-base">
|
||||
{{ $locale.baseText('template.details.viewed') }}
|
||||
{{ abbreviateNumber(template.totalViews) }}
|
||||
{{ $locale.baseText('template.details.times') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template-details-block>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import TemplateDetailsBlock from '@/components/TemplateDetailsBlock.vue';
|
||||
import HoverableNodeIcon from '@/components/HoverableNodeIcon.vue';
|
||||
|
||||
import { abbreviateNumber, filterTemplateNodes } from '@/components/helpers';
|
||||
import { ITemplatesNode } from '@/Interface';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TemplateDetails',
|
||||
props: {
|
||||
blockTitle: {
|
||||
type: String,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
HoverableNodeIcon,
|
||||
TemplateDetailsBlock,
|
||||
},
|
||||
methods: {
|
||||
abbreviateNumber,
|
||||
filterTemplateNodes,
|
||||
redirectToCategory(id: string) {
|
||||
this.$store.commit('templates/resetSessionId');
|
||||
this.$router.push(`/templates?categories=${id}`);
|
||||
},
|
||||
redirectToSearchPage(node: ITemplatesNode) {
|
||||
this.$store.commit('templates/resetSessionId');
|
||||
this.$router.push(`/templates?search=${node.displayName}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
</style>
|
38
packages/editor-ui/src/components/TemplateDetailsBlock.vue
Normal file
38
packages/editor-ui/src/components/TemplateDetailsBlock.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div :class="$style.block">
|
||||
<div :class="$style.header">
|
||||
<n8n-heading tag="h3" size="small" color="text-base">{{ title }}</n8n-heading>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TemplateDetailsBlock',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.block {
|
||||
padding-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0 0 var(--spacing-4xs);
|
||||
border-bottom: var(--border-base);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-xs) 0 0;
|
||||
}
|
||||
</style>
|
151
packages/editor-ui/src/components/TemplateFilters.vue
Normal file
151
packages/editor-ui/src/components/TemplateFilters.vue
Normal file
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div :class="$style.filters" class="template-filters">
|
||||
<div :class="$style.title" v-text="$locale.baseText('templates.categoriesHeading')" />
|
||||
<div v-if="loading" :class="$style.list">
|
||||
<n8n-loading :loading="loading" :rows="expandLimit" />
|
||||
</div>
|
||||
<ul v-if="!loading" :class="$style.categories">
|
||||
<li :class="$style.item">
|
||||
<el-checkbox
|
||||
:label="$locale.baseText('templates.allCategories')"
|
||||
:value="allSelected"
|
||||
@change="(value) => resetCategories(value)"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
v-for="category in collapsed
|
||||
? sortedCategories.slice(0, expandLimit)
|
||||
: sortedCategories"
|
||||
:key="category.id"
|
||||
:class="$style.item"
|
||||
>
|
||||
<el-checkbox
|
||||
:label="category.name"
|
||||
:value="isSelected(category.id)"
|
||||
@change="(value) => handleCheckboxChanged(value, category)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
:class="$style.button"
|
||||
v-if="sortedCategories.length > expandLimit && collapsed && !loading"
|
||||
@click="collapseAction"
|
||||
>
|
||||
<n8n-text size="small" color="primary">
|
||||
+ {{ `${sortedCategories.length - expandLimit} more` }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { ITemplatesCategory } from '@/Interface';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'TemplateFilters',
|
||||
props: {
|
||||
sortOnPopulate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
},
|
||||
expandLimit: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
selected: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
categories: {
|
||||
handler(categories: ITemplatesCategory[]) {
|
||||
if (!this.sortOnPopulate) {
|
||||
this.sortedCategories = categories;
|
||||
} else {
|
||||
const selected = this.selected || [];
|
||||
const selectedCategories = categories.filter(({ id }) => selected.includes(id));
|
||||
const notSelectedCategories = categories.filter(({ id }) => !selected.includes(id));
|
||||
this.sortedCategories = selectedCategories.concat(notSelectedCategories);
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: true,
|
||||
sortedCategories: [] as ITemplatesCategory[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allSelected(): boolean {
|
||||
return this.selected.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
collapseAction() {
|
||||
this.collapsed = false;
|
||||
},
|
||||
handleCheckboxChanged(value: boolean, selectedCategory: ITemplatesCategory) {
|
||||
this.$emit(value ? 'select' : 'clear', selectedCategory.id);
|
||||
},
|
||||
isSelected(categoryId: string) {
|
||||
return this.selected.includes(categoryId);
|
||||
},
|
||||
resetCategories() {
|
||||
this.$emit('clearAll');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.title {
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.categories {
|
||||
padding-top: var(--spacing-xs);
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-top: var(--spacing-xs);
|
||||
|
||||
&:nth-child(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
padding-top: var(--spacing-2xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.template-filters {
|
||||
.el-checkbox {
|
||||
display: flex;
|
||||
white-space: unset;
|
||||
}
|
||||
|
||||
.el-checkbox__label {
|
||||
top: -2px;
|
||||
position: relative;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--font-line-height-loose);
|
||||
color: var(--color-text-dark);
|
||||
padding-left: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
</style>
|
116
packages/editor-ui/src/components/TemplateList.vue
Normal file
116
packages/editor-ui/src/components/TemplateList.vue
Normal file
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div :class="$style.list" v-if="loading || workflows.length">
|
||||
<div :class="$style.header">
|
||||
<n8n-heading :bold="true" size="medium" color="text-light">
|
||||
{{ $locale.baseText('templates.workflows') }}
|
||||
<span v-if="!loading && totalWorkflows" v-text="`(${totalWorkflows})`" />
|
||||
</n8n-heading>
|
||||
</div>
|
||||
<div :class="$style.container">
|
||||
<TemplateCard
|
||||
v-for="(workflow, index) in workflows"
|
||||
:key="workflow.id"
|
||||
:workflow="workflow"
|
||||
:firstItem="index === 0"
|
||||
:lastItem="index === workflows.length - 1 && !loading"
|
||||
:useWorkflowButton="useWorkflowButton"
|
||||
@click="(e) => onCardClick(e, workflow.id)"
|
||||
@useWorkflow="(e) => onUseWorkflow(e, workflow.id)"
|
||||
/>
|
||||
<div v-if="infiniteScrollEnabled" ref="loader" />
|
||||
<div v-if="loading">
|
||||
<TemplateCard
|
||||
v-for="n in 4"
|
||||
:key="'index-' + n"
|
||||
:loading="true"
|
||||
:firstItem="workflows.length === 0 && n === 1"
|
||||
:lastItem="n === 4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import TemplateCard from './TemplateCard.vue';
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'TemplateList',
|
||||
props: {
|
||||
infiniteScrollEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
useWorkflowButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
workflows: {
|
||||
type: Array,
|
||||
},
|
||||
totalWorkflows: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.infiniteScrollEnabled) {
|
||||
window.addEventListener('scroll', this.onScroll);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
components: {
|
||||
TemplateCard,
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
const el = this.$refs.loader;
|
||||
if (!el || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = (el as Element).getBoundingClientRect();
|
||||
const inView =
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
||||
|
||||
if (inView) {
|
||||
this.$emit('loadMore');
|
||||
}
|
||||
},
|
||||
onCardClick(event: MouseEvent, id: string) {
|
||||
this.$emit('openTemplate', {event, id});
|
||||
},
|
||||
onUseWorkflow(event: MouseEvent, id: string) {
|
||||
this.$emit('useWorkflow', {event, id});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.header {
|
||||
padding-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.workflowButton {
|
||||
&:hover {
|
||||
.button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nodes {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -66,7 +66,7 @@ import TagsContainer from '@/components/TagsContainer.vue';
|
|||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import { convertToDisplayDate } from './helpers';
|
||||
import { WORKFLOW_OPEN_MODAL_KEY } from '../constants';
|
||||
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, WORKFLOW_OPEN_MODAL_KEY } from '../constants';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
|
@ -112,10 +112,14 @@ export default mixins(
|
|||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.filterText = '';
|
||||
this.filterTagIds = [];
|
||||
this.openDialog();
|
||||
|
||||
this.isDataLoading = true;
|
||||
await this.loadActiveWorkflows();
|
||||
await this.loadWorkflows();
|
||||
this.isDataLoading = false;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// Make sure that users can directly type in the filter
|
||||
|
@ -159,23 +163,32 @@ export default mixins(
|
|||
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(
|
||||
const confirmModal = await this.confirmModal(
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.message'),
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.cancelButtonText'),
|
||||
true,
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
return;
|
||||
} else {
|
||||
// This is used to avoid duplicating the message
|
||||
|
||||
if (confirmModal === MODAL_CONFIRMED) {
|
||||
const saved = await this.saveCurrentWorkflow({}, false);
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: data.id },
|
||||
});
|
||||
} else if (confirmModal === MODAL_CANCEL) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: data.id },
|
||||
});
|
||||
} else if (confirmModal === MODAL_CLOSE) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.$router.push({
|
||||
|
@ -186,29 +199,30 @@ export default mixins(
|
|||
this.$store.commit('ui/closeAllModals');
|
||||
}
|
||||
},
|
||||
openDialog () {
|
||||
this.isDataLoading = true;
|
||||
this.restApi().getWorkflows()
|
||||
.then(
|
||||
(data) => {
|
||||
this.workflows = data;
|
||||
|
||||
this.workflows.forEach((workflowData: IWorkflowShortResponse) => {
|
||||
workflowData.createdAt = convertToDisplayDate(workflowData.createdAt as number);
|
||||
workflowData.updatedAt = convertToDisplayDate(workflowData.updatedAt as number);
|
||||
});
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
)
|
||||
.catch(
|
||||
(error: Error) => {
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowOpen.showError.title'),
|
||||
);
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
async loadWorkflows () {
|
||||
try {
|
||||
this.workflows = await this.restApi().getWorkflows();
|
||||
this.workflows.forEach((workflowData: IWorkflowShortResponse) => {
|
||||
workflowData.createdAt = convertToDisplayDate(workflowData.createdAt as number);
|
||||
workflowData.updatedAt = convertToDisplayDate(workflowData.updatedAt as number);
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowOpen.showError.title'),
|
||||
);
|
||||
}
|
||||
},
|
||||
async loadActiveWorkflows () {
|
||||
try {
|
||||
const activeWorkflows = await this.restApi().getActiveWorkflows();
|
||||
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowOpen.couldNotLoadActiveWorkflows'),
|
||||
);
|
||||
}
|
||||
},
|
||||
workflowActiveChanged (data: { id: string, active: boolean }) {
|
||||
for (const workflow of this.workflows) {
|
||||
|
|
144
packages/editor-ui/src/components/WorkflowPreview.vue
Normal file
144
packages/editor-ui/src/components/WorkflowPreview.vue
Normal file
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<n8n-loading :loading="!showPreview" :rows="1" variant="image" />
|
||||
<iframe
|
||||
:class="{
|
||||
[$style.workflow]: !this.nodeViewDetailsOpened,
|
||||
[$style.openNDV]: this.nodeViewDetailsOpened,
|
||||
[$style.show]: this.showPreview,
|
||||
}"
|
||||
ref="preview_iframe"
|
||||
src="/workflows/demo"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'WorkflowPreview',
|
||||
props: ['loading', 'workflow'],
|
||||
data() {
|
||||
return {
|
||||
nodeViewDetailsOpened: false,
|
||||
ready: false,
|
||||
insideIframe: false,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showPreview(): boolean {
|
||||
return !this.loading && !!this.workflow && this.ready;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMouseEnter() {
|
||||
this.insideIframe = true;
|
||||
this.scrollX = window.scrollX;
|
||||
this.scrollY = window.scrollY;
|
||||
},
|
||||
onMouseLeave() {
|
||||
this.insideIframe = false;
|
||||
},
|
||||
loadWorkflow() {
|
||||
try {
|
||||
if (!this.workflow) {
|
||||
throw new Error(this.$locale.baseText('workflowPreview.showError.missingWorkflow'));
|
||||
}
|
||||
if (!this.workflow.nodes || !Array.isArray(this.workflow.nodes)) {
|
||||
throw new Error(this.$locale.baseText('workflowPreview.showError.arrayEmpty'));
|
||||
}
|
||||
|
||||
const iframe = this.$refs.preview_iframe as HTMLIFrameElement;
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
JSON.stringify({
|
||||
command: 'openWorkflow',
|
||||
workflow: this.workflow,
|
||||
}),
|
||||
'*',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowPreview.showError.previewError.title'),
|
||||
this.$locale.baseText('workflowPreview.showError.previewError.message'),
|
||||
);
|
||||
}
|
||||
},
|
||||
receiveMessage({ data }: MessageEvent) {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json.command === 'n8nReady') {
|
||||
this.ready = true;
|
||||
} else if (json.command === 'openNDV') {
|
||||
this.nodeViewDetailsOpened = true;
|
||||
} else if (json.command === 'closeNDV') {
|
||||
this.nodeViewDetailsOpened = false;
|
||||
} else if (json.command === 'error') {
|
||||
this.$emit('close');
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
},
|
||||
onDocumentScroll() {
|
||||
if (this.insideIframe) {
|
||||
window.scrollTo(this.scrollX, this.scrollY);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showPreview(show) {
|
||||
if (show) {
|
||||
this.loadWorkflow();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('message', this.receiveMessage);
|
||||
document.addEventListener('scroll', this.onDocumentScroll);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('message', this.receiveMessage);
|
||||
document.removeEventListener('scroll', this.onDocumentScroll);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.workflow {
|
||||
border: var(--border-base);
|
||||
border-radius: var(--border-radius-large);
|
||||
|
||||
// firefox bug requires loading iframe as such
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.show {
|
||||
visibility: visible;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.openNDV {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999999;
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,21 @@
|
|||
import { ERROR_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { INodeUi } from '@/Interface';
|
||||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, TEMPLATES_NODES_FILTER } from '@/constants';
|
||||
import { INodeUi, ITemplatesNode } from '@/Interface';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
|
||||
const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
|
||||
|
||||
export function abbreviateNumber(num: number) {
|
||||
const tier = (Math.log10(Math.abs(num)) / 3) | 0;
|
||||
|
||||
if (tier === 0) return num;
|
||||
|
||||
const suffix = SI_SYMBOL[tier];
|
||||
const scale = Math.pow(10, tier * 3);
|
||||
const scaled = num / scale;
|
||||
|
||||
return Number(scaled.toFixed(1)) + suffix;
|
||||
}
|
||||
|
||||
export function convertToDisplayDate (epochTime: number) {
|
||||
return dateformat(epochTime, 'yyyy-mm-dd HH:MM:ss');
|
||||
|
@ -31,3 +44,18 @@ export function getActivatableTriggerNodes(nodes: INodeUi[]) {
|
|||
return !node.disabled && node.type !== ERROR_TRIGGER_NODE_TYPE;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterTemplateNodes(nodes: ITemplatesNode[]) {
|
||||
const notCoreNodes = nodes.filter((node: ITemplatesNode) => {
|
||||
return !(node.categories || []).some(
|
||||
(category) => category.name === CORE_NODES_CATEGORY,
|
||||
);
|
||||
});
|
||||
|
||||
const results = notCoreNodes.length > 0 ? notCoreNodes : nodes;
|
||||
return results.filter((elem) => !TEMPLATES_NODES_FILTER.includes(elem.name));
|
||||
}
|
||||
|
||||
export function setPageTitle(title: string) {
|
||||
window.document.title = title;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ export const copyPaste = Vue.extend({
|
|||
data () {
|
||||
return {
|
||||
copyPasteElementsGotCreated: false,
|
||||
hiddenInput: null as null | Element,
|
||||
onPaste: null as null | Function,
|
||||
onBeforePaste: null as null | Function,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
|
@ -53,6 +56,7 @@ export const copyPaste = Vue.extend({
|
|||
hiddenInput.setAttribute('type', 'text');
|
||||
hiddenInput.setAttribute('id', 'hidden-input-copy-paste');
|
||||
hiddenInput.setAttribute('class', 'hidden-copy-paste');
|
||||
this.hiddenInput = hiddenInput;
|
||||
|
||||
document.body.append(hiddenInput);
|
||||
|
||||
|
@ -64,12 +68,14 @@ export const copyPaste = Vue.extend({
|
|||
ieClipboardDiv.setAttribute('contenteditable', 'true');
|
||||
document.body.append(ieClipboardDiv);
|
||||
|
||||
document.addEventListener('beforepaste', () => {
|
||||
this.onBeforePaste = () => {
|
||||
// @ts-ignore
|
||||
if (hiddenInput.is(':focus')) {
|
||||
this.focusIeClipboardDiv(ieClipboardDiv as HTMLDivElement);
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
// @ts-ignore
|
||||
document.addEventListener('beforepaste', this.onBeforePaste, true);
|
||||
}
|
||||
|
||||
let userInput = '';
|
||||
|
@ -90,36 +96,38 @@ export const copyPaste = Vue.extend({
|
|||
}
|
||||
});
|
||||
|
||||
// Set clipboard event listeners on the document.
|
||||
['paste'].forEach((event) => {
|
||||
document.addEventListener(event, debounce((e) => {
|
||||
// Check if the event got emitted from a message box or from something
|
||||
// else which should ignore the copy/paste
|
||||
// @ts-ignore
|
||||
const path = e.path || (e.composedPath && e.composedPath());
|
||||
for (let index = 0; index < path.length; index++) {
|
||||
if (path[index].className && typeof path[index].className === 'string' && (
|
||||
path[index].className.includes('el-message-box') || path[index].className.includes('ignore-key-press')
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
this.onPaste = debounce((e) => {
|
||||
const event = 'paste';
|
||||
// Check if the event got emitted from a message box or from something
|
||||
// else which should ignore the copy/paste
|
||||
// @ts-ignore
|
||||
const path = e.path || (e.composedPath && e.composedPath());
|
||||
for (let index = 0; index < path.length; index++) {
|
||||
if (path[index].className && typeof path[index].className === 'string' && (
|
||||
path[index].className.includes('el-message-box') || path[index].className.includes('ignore-key-press')
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ieClipboardDiv !== null) {
|
||||
this.ieClipboardEvent(event, ieClipboardDiv);
|
||||
} else {
|
||||
this.standardClipboardEvent(event, e as ClipboardEvent);
|
||||
// @ts-ignore
|
||||
if (!document.activeElement || (document.activeElement && ['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)) {
|
||||
// That it still allows to paste into text, email, password & textarea-fiels we
|
||||
// check if we can identify the active element and if so only
|
||||
// run it if something else is selected.
|
||||
this.focusHiddenArea(hiddenInput);
|
||||
e.preventDefault();
|
||||
}
|
||||
if (ieClipboardDiv !== null) {
|
||||
this.ieClipboardEvent(event, ieClipboardDiv);
|
||||
} else {
|
||||
this.standardClipboardEvent(event, e as ClipboardEvent);
|
||||
// @ts-ignore
|
||||
if (!document.activeElement || (document.activeElement && ['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)) {
|
||||
// That it still allows to paste into text, email, password & textarea-fiels we
|
||||
// check if we can identify the active element and if so only
|
||||
// run it if something else is selected.
|
||||
this.focusHiddenArea(hiddenInput);
|
||||
e.preventDefault();
|
||||
}
|
||||
}, 1000, { leading: true }));
|
||||
});
|
||||
}
|
||||
}, 1000, { leading: true });
|
||||
|
||||
// Set clipboard event listeners on the document.
|
||||
// @ts-ignore
|
||||
document.addEventListener('paste', this.onPaste);
|
||||
},
|
||||
methods: {
|
||||
receivedCopyPasteData (plainTextData: string, event?: ClipboardEvent): void {
|
||||
|
@ -198,4 +206,17 @@ export const copyPaste = Vue.extend({
|
|||
},
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.remove();
|
||||
}
|
||||
if (this.onPaste) {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('paste', this.onPaste);
|
||||
}
|
||||
if (this.onBeforePaste) {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('beforepaste', this.onBeforePaste);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -77,11 +77,12 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
async callDebounced (...inputParameters: any[]): Promise<void> { // tslint:disable-line:no-any
|
||||
const functionName = inputParameters.shift() as string;
|
||||
const debounceTime = inputParameters.shift() as number;
|
||||
const trailing = inputParameters.shift() as boolean;
|
||||
|
||||
// @ts-ignore
|
||||
if (this.debouncedFunctions[functionName] === undefined) {
|
||||
// @ts-ignore
|
||||
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, { leading: true });
|
||||
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing: true } : { leading: true } );
|
||||
}
|
||||
// @ts-ignore
|
||||
await this.debouncedFunctions[functionName].apply(this, inputParameters);
|
||||
|
|
|
@ -150,6 +150,23 @@ export const showMessage = mixins(externalHooks).extend({
|
|||
}
|
||||
},
|
||||
|
||||
async confirmModal (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText?: string, cancelButtonText?: string, showClose = false): Promise<string> {
|
||||
try {
|
||||
const options: ElMessageBoxOptions = {
|
||||
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
|
||||
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
|
||||
dangerouslyUseHTMLString: true,
|
||||
showClose,
|
||||
...(type && { type }),
|
||||
};
|
||||
|
||||
await this.$confirm(message, headline, options);
|
||||
return 'confirmed';
|
||||
} catch (e) {
|
||||
return e as string;
|
||||
}
|
||||
},
|
||||
|
||||
clearAllStickyNotifications() {
|
||||
stickyNotificationQueue.map((notification: ElNotificationComponent) => {
|
||||
if (notification) {
|
||||
|
|
|
@ -451,10 +451,10 @@ export const workflowHelpers = mixins(
|
|||
}
|
||||
},
|
||||
|
||||
async saveCurrentWorkflow({name, tags}: {name?: string, tags?: string[]} = {}): Promise<boolean> {
|
||||
async saveCurrentWorkflow({name, tags}: {name?: string, tags?: string[]} = {}, redirect = true): Promise<boolean> {
|
||||
const currentWorkflow = this.$route.params.name;
|
||||
if (!currentWorkflow) {
|
||||
return this.saveAsNewWorkflow({name, tags});
|
||||
return this.saveAsNewWorkflow({name, tags}, redirect);
|
||||
}
|
||||
|
||||
// Workflow exists already so update it
|
||||
|
@ -501,7 +501,7 @@ export const workflowHelpers = mixins(
|
|||
}
|
||||
},
|
||||
|
||||
async saveAsNewWorkflow ({name, tags, resetWebhookUrls, openInNewWindow}: {name?: string, tags?: string[], resetWebhookUrls?: boolean, openInNewWindow?: boolean} = {}): Promise<boolean> {
|
||||
async saveAsNewWorkflow ({name, tags, resetWebhookUrls, openInNewWindow}: {name?: string, tags?: string[], resetWebhookUrls?: boolean, openInNewWindow?: boolean} = {}, redirect = true): Promise<boolean> {
|
||||
try {
|
||||
this.$store.commit('addActiveAction', 'workflowSaving');
|
||||
|
||||
|
@ -552,10 +552,21 @@ export const workflowHelpers = mixins(
|
|||
const tagIds = createdTags.map((tag: ITag): string => tag.id);
|
||||
this.$store.commit('setWorkflowTagIds', tagIds);
|
||||
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
||||
});
|
||||
const templateId = this.$route.query.templateId;
|
||||
if (templateId) {
|
||||
this.$telemetry.track('User saved new workflow from template', {
|
||||
template_id: templateId,
|
||||
workflow_id: workflowData.id,
|
||||
wf_template_repo_session_id: this.$store.getters['templates/previousSessionId'],
|
||||
});
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
||||
});
|
||||
}
|
||||
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
@ -567,7 +578,7 @@ export const workflowHelpers = mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('workflowHelpers.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowHelpers.showMessage.message') + `"${e.message}"`,
|
||||
message: (e as Error).message,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
|
|
|
@ -38,8 +38,7 @@ export const BREAKPOINT_LG = 1200;
|
|||
export const BREAKPOINT_XL = 1920;
|
||||
|
||||
|
||||
// templates
|
||||
export const TEMPLATES_BASE_URL = `https://api.n8n.io/`;
|
||||
export const N8N_IO_BASE_URL = `https://api.n8n.io/`;
|
||||
|
||||
// node types
|
||||
export const CALENDLY_TRIGGER_NODE_TYPE = 'n8n-nodes-base.calendlyTrigger';
|
||||
|
@ -136,6 +135,36 @@ export const CODING_SKILL_KEY = 'codingSkill';
|
|||
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||
export const OTHER_COMPANY_INDUSTRY_KEY = 'otherCompanyIndustry';
|
||||
|
||||
export const MODAL_CANCEL = 'cancel';
|
||||
export const MODAL_CLOSE = 'close';
|
||||
export const MODAL_CONFIRMED = 'confirmed';
|
||||
|
||||
export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
||||
|
||||
export const HIRING_BANNER = `
|
||||
//////
|
||||
///////////
|
||||
///// ////
|
||||
/////////////////// ////
|
||||
////////////////////// ////
|
||||
/////// /////// //// /////////////
|
||||
//////////// //////////// //// ///////
|
||||
//// //// //// //// ////
|
||||
///// ///////////// //////////
|
||||
///// //// //// //// ////
|
||||
//////////// //////////// //// ////////
|
||||
/////// ////// //// /////////////
|
||||
///////////// ////
|
||||
////////// ////
|
||||
//// ////
|
||||
///////////
|
||||
//////
|
||||
|
||||
Love n8n? Help us build the future of automation! https://n8n.io/careers
|
||||
`;
|
||||
|
||||
export const TEMPLATES_NODES_FILTER = [
|
||||
'n8n-nodes-base.start',
|
||||
'n8n-nodes-base.respondToWebhook',
|
||||
];
|
||||
|
|
|
@ -123,10 +123,16 @@ const module: Module<ICredentialsState, IRootState> = {
|
|||
},
|
||||
actions: {
|
||||
fetchCredentialTypes: async (context: ActionContext<ICredentialsState, IRootState>) => {
|
||||
if (context.getters.allCredentialTypes.length > 0) {
|
||||
return;
|
||||
}
|
||||
const credentialTypes = await getCredentialTypes(context.rootGetters.getRestApiContext);
|
||||
context.commit('setCredentialTypes', credentialTypes);
|
||||
},
|
||||
fetchAllCredentials: async (context: ActionContext<ICredentialsState, IRootState>) => {
|
||||
if (context.getters.allCredentials.length > 0) {
|
||||
return;
|
||||
}
|
||||
const credentials = await getAllCredentials(context.rootGetters.getRestApiContext);
|
||||
context.commit('setCredentials', credentials);
|
||||
},
|
||||
|
|
|
@ -13,12 +13,14 @@ import Vue from 'vue';
|
|||
import { getPersonalizedNodeTypes } from './helper';
|
||||
import { CONTACT_PROMPT_MODAL_KEY, PERSONALIZATION_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||
import { ITelemetrySettings } from 'n8n-workflow';
|
||||
import { testHealthEndpoint } from '@/api/templates';
|
||||
|
||||
const module: Module<ISettingsState, IRootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
settings: {} as IN8nUISettings,
|
||||
promptsData: {} as IN8nPrompts,
|
||||
templatesEndpointHealthy: false,
|
||||
},
|
||||
getters: {
|
||||
personalizedNodeTypes(state: ISettingsState): string[] {
|
||||
|
@ -41,6 +43,18 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
isTelemetryEnabled: (state) => {
|
||||
return state.settings.telemetry && state.settings.telemetry.enabled;
|
||||
},
|
||||
isInternalUser: (state): boolean => {
|
||||
return state.settings.deploymentType === 'n8n-internal';
|
||||
},
|
||||
isTemplatesEnabled: (state): boolean => {
|
||||
return Boolean(state.settings.templates && state.settings.templates.enabled);
|
||||
},
|
||||
isTemplatesEndpointReachable: (state): boolean => {
|
||||
return state.templatesEndpointHealthy;
|
||||
},
|
||||
templatesHost: (state): string => {
|
||||
return state.settings.templates.host;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setSettings(state: ISettingsState, settings: IN8nUISettings) {
|
||||
|
@ -55,6 +69,9 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
setPromptsData(state: ISettingsState, promptsData: IN8nPrompts) {
|
||||
Vue.set(state, 'promptsData', promptsData);
|
||||
},
|
||||
setTemplatesEndpointHealthy(state: ISettingsState) {
|
||||
state.templatesEndpointHealthy = true;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getSettings(context: ActionContext<ISettingsState, IRootState>) {
|
||||
|
@ -124,6 +141,11 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
return e;
|
||||
}
|
||||
},
|
||||
async testTemplatesEndpoint(context: ActionContext<ISettingsState, IRootState>) {
|
||||
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
|
||||
await Promise.race([testHealthEndpoint(context.getters.templatesHost), timeout]);
|
||||
context.commit('setTemplatesEndpointHealthy', true);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
282
packages/editor-ui/src/modules/templates.ts
Normal file
282
packages/editor-ui/src/modules/templates.ts
Normal file
|
@ -0,0 +1,282 @@
|
|||
import { getCategories, getCollectionById, getCollections, getTemplateById, getWorkflows, getWorkflowTemplate } from '@/api/templates';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
IRootState,
|
||||
ITemplatesCollection,
|
||||
ITemplatesWorkflow,
|
||||
ITemplatesCategory,
|
||||
ITemplateState,
|
||||
ITemplatesQuery,
|
||||
ITemplatesWorkflowFull,
|
||||
ITemplatesCollectionFull,
|
||||
IWorkflowTemplate,
|
||||
} from '../Interface';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
const TEMPLATES_PAGE_SIZE = 10;
|
||||
|
||||
function getSearchKey(query: ITemplatesQuery): string {
|
||||
return JSON.stringify([query.search || '', [...query.categories].sort()]);
|
||||
}
|
||||
|
||||
const module: Module<ITemplateState, IRootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
categories: {},
|
||||
collections: {},
|
||||
workflows: {},
|
||||
collectionSearches: {},
|
||||
workflowSearches: {},
|
||||
currentSessionId: '',
|
||||
previousSessionId: '',
|
||||
},
|
||||
getters: {
|
||||
allCategories(state: ITemplateState) {
|
||||
return Object.values(state.categories).sort((a: ITemplatesCategory, b: ITemplatesCategory) => a.name > b.name ? 1: -1);
|
||||
},
|
||||
getTemplateById(state: ITemplateState) {
|
||||
return (id: string): null | ITemplatesWorkflow => state.workflows[id];
|
||||
},
|
||||
getCollectionById(state: ITemplateState) {
|
||||
return (id: string): null | ITemplatesCollection => state.collections[id];
|
||||
},
|
||||
getCategoryById(state: ITemplateState) {
|
||||
return (id: string): null | ITemplatesCategory => state.categories[id];
|
||||
},
|
||||
getSearchedCollections(state: ITemplateState) {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = state.collectionSearches[searchKey];
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return search.collectionIds.map((collectionId: string) => state.collections[collectionId]);
|
||||
};
|
||||
},
|
||||
getSearchedWorkflows(state: ITemplateState) {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = state.workflowSearches[searchKey];
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return search.workflowIds.map((workflowId: string) => state.workflows[workflowId]);
|
||||
};
|
||||
},
|
||||
getSearchedWorkflowsTotal(state: ITemplateState) {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = state.workflowSearches[searchKey];
|
||||
|
||||
return search ? search.totalWorkflows : 0;
|
||||
};
|
||||
},
|
||||
isSearchLoadingMore(state: ITemplateState) {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = state.workflowSearches[searchKey];
|
||||
|
||||
return Boolean(search && search.loadingMore);
|
||||
};
|
||||
},
|
||||
isSearchFinished(state: ITemplateState) {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = state.workflowSearches[searchKey];
|
||||
|
||||
return Boolean(search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length);
|
||||
};
|
||||
},
|
||||
currentSessionId(state: ITemplateState) {
|
||||
return state.currentSessionId;
|
||||
},
|
||||
previousSessionId(state: ITemplateState) {
|
||||
return state.previousSessionId;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addCategories(state: ITemplateState, categories: ITemplatesCategory[]) {
|
||||
categories.forEach((category: ITemplatesCategory) => {
|
||||
Vue.set(state.categories, category.id, category);
|
||||
});
|
||||
},
|
||||
addCollections(state: ITemplateState, collections: Array<ITemplatesCollection | ITemplatesCollectionFull>) {
|
||||
collections.forEach((collection) => {
|
||||
const workflows = (collection.workflows || []).map((workflow) => ({id: workflow.id}));
|
||||
const cachedCollection = state.collections[collection.id] || {};
|
||||
Vue.set(state.collections, collection.id, {
|
||||
...cachedCollection,
|
||||
...collection,
|
||||
workflows,
|
||||
});
|
||||
});
|
||||
},
|
||||
addWorkflows(state: ITemplateState, workflows: Array<ITemplatesWorkflow | ITemplatesWorkflowFull>) {
|
||||
workflows.forEach((workflow: ITemplatesWorkflow) => {
|
||||
const cachedWorkflow = state.workflows[workflow.id] || {};
|
||||
Vue.set(state.workflows, workflow.id, {
|
||||
...cachedWorkflow,
|
||||
...workflow,
|
||||
});
|
||||
});
|
||||
},
|
||||
addCollectionSearch(state: ITemplateState, data: {collections: ITemplatesCollection[], query: ITemplatesQuery}) {
|
||||
const collectionIds = data.collections.map((collection) => collection.id);
|
||||
const searchKey = getSearchKey(data.query);
|
||||
Vue.set(state.collectionSearches, searchKey, {
|
||||
collectionIds,
|
||||
});
|
||||
},
|
||||
addWorkflowsSearch(state: ITemplateState, data: {totalWorkflows: number; workflows: ITemplatesWorkflow[], query: ITemplatesQuery}) {
|
||||
const workflowIds = data.workflows.map((workflow) => workflow.id);
|
||||
const searchKey = getSearchKey(data.query);
|
||||
const cachedResults = state.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
Vue.set(state.workflowSearches, searchKey, {
|
||||
workflowIds,
|
||||
totalWorkflows: data.totalWorkflows,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(state.workflowSearches, searchKey, {
|
||||
workflowIds: [...cachedResults.workflowIds, ...workflowIds],
|
||||
totalWorkflows: data.totalWorkflows,
|
||||
});
|
||||
},
|
||||
setWorkflowSearchLoading(state: ITemplateState, query: ITemplatesQuery) {
|
||||
const searchKey = getSearchKey(query);
|
||||
const cachedResults = state.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(state.workflowSearches[searchKey], 'loadingMore', true);
|
||||
},
|
||||
setWorkflowSearchLoaded(state: ITemplateState, query: ITemplatesQuery) {
|
||||
const searchKey = getSearchKey(query);
|
||||
const cachedResults = state.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(state.workflowSearches[searchKey], 'loadingMore', false);
|
||||
},
|
||||
resetSessionId(state: ITemplateState) {
|
||||
state.previousSessionId = state.currentSessionId;
|
||||
state.currentSessionId = '';
|
||||
},
|
||||
setSessionId(state: ITemplateState) {
|
||||
if (!state.currentSessionId) {
|
||||
state.currentSessionId = `templates-${Date.now()}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getTemplateById(context: ActionContext<ITemplateState, IRootState>, templateId: string): Promise<ITemplatesWorkflowFull> {
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
const response = await getTemplateById(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
||||
const template: ITemplatesWorkflowFull = {
|
||||
...response.workflow,
|
||||
full: true,
|
||||
};
|
||||
|
||||
context.commit('addWorkflows', [template]);
|
||||
return template;
|
||||
},
|
||||
async getCollectionById(context: ActionContext<ITemplateState, IRootState>, collectionId: string): Promise<ITemplatesCollection> {
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
const response = await getCollectionById(apiEndpoint, collectionId, { 'n8n-version': versionCli });
|
||||
const collection: ITemplatesCollectionFull = {
|
||||
...response.collection,
|
||||
full: true,
|
||||
};
|
||||
|
||||
context.commit('addCollections', [collection]);
|
||||
context.commit('addWorkflows', response.collection.workflows);
|
||||
|
||||
return context.getters.getCollectionById(collectionId);
|
||||
},
|
||||
async getCategories(context: ActionContext<ITemplateState, IRootState>): Promise<ITemplatesCategory[]> {
|
||||
const cachedCategories: ITemplatesCategory[] = context.getters.allCategories;
|
||||
if (cachedCategories.length) {
|
||||
return cachedCategories;
|
||||
}
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli });
|
||||
const categories = response.categories;
|
||||
|
||||
context.commit('addCategories', categories);
|
||||
|
||||
return categories;
|
||||
},
|
||||
async getCollections(context: ActionContext<ITemplateState, IRootState>, query: ITemplatesQuery): Promise<ITemplatesCollection[]> {
|
||||
const cachedResults: ITemplatesCollection[] | null = context.getters.getSearchedCollections(query);
|
||||
if (cachedResults) {
|
||||
return cachedResults;
|
||||
}
|
||||
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli });
|
||||
const collections = response.collections;
|
||||
|
||||
context.commit('addCollections', collections);
|
||||
context.commit('addCollectionSearch', {query, collections});
|
||||
collections.forEach((collection: ITemplatesCollection) => context.commit('addWorkflows', collection.workflows));
|
||||
|
||||
return collections;
|
||||
},
|
||||
async getWorkflows(context: ActionContext<ITemplateState, IRootState>, query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> {
|
||||
const cachedResults: ITemplatesWorkflow[] = context.getters.getSearchedWorkflows(query);
|
||||
if (cachedResults) {
|
||||
return cachedResults;
|
||||
}
|
||||
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
|
||||
const payload = await getWorkflows(apiEndpoint, {...query, skip: 0, limit: TEMPLATES_PAGE_SIZE}, { 'n8n-version': versionCli });
|
||||
|
||||
context.commit('addWorkflows', payload.workflows);
|
||||
context.commit('addWorkflowsSearch', {...payload, query});
|
||||
|
||||
return context.getters.getSearchedWorkflows(query);
|
||||
},
|
||||
async getMoreWorkflows(context: ActionContext<ITemplateState, IRootState>, query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> {
|
||||
if (context.getters.isSearchLoadingMore(query) && !context.getters.isSearchFinished(query)) {
|
||||
return [];
|
||||
}
|
||||
const cachedResults: ITemplatesWorkflow[] = context.getters.getSearchedWorkflows(query) || [];
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
|
||||
context.commit('setWorkflowSearchLoading', query);
|
||||
try {
|
||||
const payload = await getWorkflows(apiEndpoint, {...query, skip: cachedResults.length, limit: TEMPLATES_PAGE_SIZE});
|
||||
|
||||
context.commit('setWorkflowSearchLoaded', query);
|
||||
context.commit('addWorkflows', payload.workflows);
|
||||
context.commit('addWorkflowsSearch', {...payload, query});
|
||||
|
||||
return context.getters.getSearchedWorkflows(query);
|
||||
} catch (e) {
|
||||
context.commit('setWorkflowSearchLoaded', query);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
getWorkflowTemplate: async (context: ActionContext<ITemplateState, IRootState>, templateId: string): Promise<IWorkflowTemplate> => {
|
||||
const apiEndpoint: string = context.rootGetters['settings/templatesHost'];
|
||||
const versionCli: string = context.rootGetters['versionCli'];
|
||||
return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default module;
|
|
@ -55,8 +55,12 @@ const module: Module<IUiState, IRootState> = {
|
|||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
isPageLoading: true,
|
||||
currentView: '',
|
||||
},
|
||||
getters: {
|
||||
areExpressionsDisabled(state: IUiState) {
|
||||
return state.currentView === 'WorkflowDemo';
|
||||
},
|
||||
isVersionsOpen: (state: IUiState) => {
|
||||
return state.modals[VERSIONS_MODAL_KEY].open;
|
||||
},
|
||||
|
@ -104,6 +108,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
toggleSidebarMenuCollapse: (state: IUiState) => {
|
||||
state.sidebarMenuCollapsed = !state.sidebarMenuCollapsed;
|
||||
},
|
||||
setCurrentView: (state: IUiState, currentView: string) => {
|
||||
state.currentView = currentView;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { getNewWorkflow, getWorkflowTemplate } from '@/api/workflows';
|
||||
import { getNewWorkflow } from '@/api/workflows';
|
||||
import { DUPLICATE_POSTFFIX, MAX_WORKFLOW_NAME_LENGTH, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
IRootState,
|
||||
IWorkflowsState,
|
||||
IWorkflowTemplate,
|
||||
} from '../Interface';
|
||||
|
||||
const module: Module<IWorkflowsState, IRootState> = {
|
||||
|
@ -39,14 +38,11 @@ const module: Module<IWorkflowsState, IRootState> = {
|
|||
newName = newWorkflow.name;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
return newName;
|
||||
},
|
||||
getWorkflowTemplate: async (context: ActionContext<IWorkflowsState, IRootState>, templateId: string): Promise<IWorkflowTemplate> => {
|
||||
return await getWorkflowTemplate(templateId);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default module;
|
||||
export default module;
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
@import "~n8n-design-system/theme/dist/index.css";
|
||||
|
||||
|
||||
body {
|
||||
background-color: var(--color-canvas-background);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import MessageBox from 'element-ui/lib/message-box';
|
|||
import Message from 'element-ui/lib/message';
|
||||
import Notification from 'element-ui/lib/notification';
|
||||
import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
|
||||
import VueAgile from 'vue-agile';
|
||||
|
||||
// @ts-ignore
|
||||
import lang from 'element-ui/lib/locale/lang/en';
|
||||
|
@ -50,12 +51,16 @@ import {
|
|||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nInputNumber,
|
||||
N8nLoading,
|
||||
N8nHeading,
|
||||
N8nMarkdown,
|
||||
N8nMenu,
|
||||
N8nMenuItem,
|
||||
N8nSelect,
|
||||
N8nSpinner,
|
||||
N8nSquareButton,
|
||||
N8nTags,
|
||||
N8nTag,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
N8nOption,
|
||||
|
@ -71,12 +76,16 @@ Vue.use(N8nInfoTip);
|
|||
Vue.use(N8nInput);
|
||||
Vue.use(N8nInputLabel);
|
||||
Vue.use(N8nInputNumber);
|
||||
Vue.component('n8n-loading', N8nLoading);
|
||||
Vue.use(N8nHeading);
|
||||
Vue.component('n8n-markdown', N8nMarkdown);
|
||||
Vue.use(N8nMenu);
|
||||
Vue.use(N8nMenuItem);
|
||||
Vue.use(N8nSelect);
|
||||
Vue.use(N8nSpinner);
|
||||
Vue.component('n8n-square-button', N8nSquareButton);
|
||||
Vue.use(N8nTags);
|
||||
Vue.use(N8nTag);
|
||||
Vue.component('n8n-text', N8nText);
|
||||
Vue.use(N8nTooltip);
|
||||
Vue.use(N8nOption);
|
||||
|
@ -111,6 +120,7 @@ Vue.use(Badge);
|
|||
Vue.use(Card);
|
||||
Vue.use(ColorPicker);
|
||||
Vue.use(Container);
|
||||
Vue.use(VueAgile);
|
||||
|
||||
Vue.component(CollapseTransition.name, CollapseTransition);
|
||||
|
||||
|
@ -141,7 +151,8 @@ Vue.prototype.$confirm = async (message: string, configOrTitle: string | ElMessa
|
|||
roundButton: true,
|
||||
cancelButtonClass: 'btn--cancel',
|
||||
confirmButtonClass: 'btn--confirm',
|
||||
showClose: false,
|
||||
distinguishCancelAndClose: true,
|
||||
showClose: config.showClose || false,
|
||||
closeOnClickModal: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -131,7 +131,8 @@
|
|||
},
|
||||
"type": "Type",
|
||||
"updated": "Updated",
|
||||
"yourSavedCredentials": "Your saved credentials"
|
||||
"yourSavedCredentials": "Your saved credentials",
|
||||
"errorLoadingCredentials": "Error loading credentials"
|
||||
},
|
||||
"dataDisplay": {
|
||||
"needHelp": "Need help?",
|
||||
|
@ -289,10 +290,10 @@
|
|||
"message": "Are you sure that you want to delete '{workflowName}'?"
|
||||
},
|
||||
"workflowNew": {
|
||||
"cancelButtonText": "",
|
||||
"confirmButtonText": "Yes, switch workflows and forget changes",
|
||||
"headline": "Switch workflows without saving?",
|
||||
"message": "When you switch workflows without saving, your current changes will be lost."
|
||||
"cancelButtonText": "Leave without saving",
|
||||
"confirmButtonText": "Save",
|
||||
"headline": "Save changes before leaving?",
|
||||
"message": "If you don't save, you will lose your changes."
|
||||
}
|
||||
},
|
||||
"credentials": "Credentials",
|
||||
|
@ -309,6 +310,7 @@
|
|||
"importFromFile": "Import from File",
|
||||
"importFromUrl": "Import from URL",
|
||||
"new": "New",
|
||||
"newTemplate": "New from template",
|
||||
"open": "Open",
|
||||
"prompt": {
|
||||
"cancel": "@:reusableBaseText.cancel",
|
||||
|
@ -342,6 +344,7 @@
|
|||
"title": "Execution stopped"
|
||||
}
|
||||
},
|
||||
"templates": "Templates",
|
||||
"workflows": "Workflows"
|
||||
},
|
||||
"multipleParameter": {
|
||||
|
@ -488,16 +491,16 @@
|
|||
"addNode": "Add node",
|
||||
"confirmMessage": {
|
||||
"beforeRouteLeave": {
|
||||
"cancelButtonText": "",
|
||||
"confirmButtonText": "Yes, switch workflows and forget changes",
|
||||
"headline": "Switch workflows without saving?",
|
||||
"message": "When you switch workflows without saving, your current changes will be lost."
|
||||
"cancelButtonText": "Leave without saving",
|
||||
"confirmButtonText": "Save",
|
||||
"headline": "Save changes before leaving?",
|
||||
"message": "If you don't save, you will lose your changes."
|
||||
},
|
||||
"initView": {
|
||||
"cancelButtonText": "",
|
||||
"confirmButtonText": "Yes, switch workflows and forget changes",
|
||||
"headline": "Switch workflows without saving?",
|
||||
"message": "When you switch workflows without saving, your current changes will be lost."
|
||||
"cancelButtonText": "Leave without saving",
|
||||
"confirmButtonText": "Save",
|
||||
"headline": "Save changes before leaving?",
|
||||
"message": "If you don't save, you will lose your changes."
|
||||
},
|
||||
"receivedCopyPasteData": {
|
||||
"cancelButtonText": "",
|
||||
|
@ -620,7 +623,8 @@
|
|||
"refreshList": "Refresh List",
|
||||
"removeExpression": "Remove Expression",
|
||||
"resetValue": "Reset Value",
|
||||
"selectDateAndTime": "Select date and time"
|
||||
"selectDateAndTime": "Select date and time",
|
||||
"select": "Select"
|
||||
},
|
||||
"parameterInputExpanded": {
|
||||
"openDocs": "Open docs",
|
||||
|
@ -729,6 +733,14 @@
|
|||
"saved": "Saved",
|
||||
"saving": "Saving"
|
||||
},
|
||||
"settings": {
|
||||
"errors": {
|
||||
"connectionError": {
|
||||
"title": "Error connecting to n8n",
|
||||
"message": "Could not connect to server. <a onclick='window.location.reload(false);'>Refresh</a> to try again"
|
||||
}
|
||||
}
|
||||
},
|
||||
"showMessage": {
|
||||
"cancel": "@:reusableBaseText.cancel",
|
||||
"ok": "OK",
|
||||
|
@ -795,6 +807,38 @@
|
|||
},
|
||||
"notBeingUsed": "Not being used"
|
||||
},
|
||||
"template": {
|
||||
"buttons": {
|
||||
"goBackButton": "Go back",
|
||||
"useThisWorkflowButton": "Use this workflow"
|
||||
},
|
||||
"details": {
|
||||
"appsInTheWorkflow": "Apps in this workflow",
|
||||
"appsInTheCollection": "This collection features",
|
||||
"by": "by",
|
||||
"categories": "Categories",
|
||||
"created": "Created",
|
||||
"details": "Details",
|
||||
"times": "times",
|
||||
"viewed": "Viewed"
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"allCategories": "All Categories",
|
||||
"categoriesHeading": "Categories",
|
||||
"collection": "Collection",
|
||||
"collections": "Collections",
|
||||
"collectionsNotFound": "Collection could not be found",
|
||||
"endResult": "Share your own useful workflows through your <a href='https://n8n.io/dashboard' target='_blank'>n8n.io account</a>",
|
||||
"heading": "Workflow templates",
|
||||
"newButton": "New blank workflow",
|
||||
"noSearchResults": "Nothing found. Try adjusting your search to see more.",
|
||||
"searchPlaceholder": "Search workflows",
|
||||
"workflow": "Workflow",
|
||||
"workflows": "Workflows",
|
||||
"workflowsNotFound": "Workflow could not be found",
|
||||
"connectionWarning": "⚠️ There was a problem fetching workflow templates. Check your internet connection."
|
||||
},
|
||||
"textEdit": {
|
||||
"edit": "Edit"
|
||||
},
|
||||
|
@ -898,10 +942,10 @@
|
|||
"workflowOpen": {
|
||||
"active": "Active",
|
||||
"confirmMessage": {
|
||||
"cancelButtonText": "",
|
||||
"confirmButtonText": "Yes, switch workflows and forget changes",
|
||||
"headline": "Switch workflows without saving?",
|
||||
"message": "If you do this, your current changes will be lost."
|
||||
"cancelButtonText": "Leave without saving",
|
||||
"confirmButtonText": "Save",
|
||||
"headline": "Save changes before leaving?",
|
||||
"message": "If you don't save, you will lose your changes."
|
||||
},
|
||||
"created": "Created",
|
||||
"name": "@:reusableBaseText.name",
|
||||
|
@ -915,7 +959,8 @@
|
|||
"message": "This is the current workflow",
|
||||
"title": "Workflow already open"
|
||||
},
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"couldNotLoadActiveWorkflows": "Could not load active workflows"
|
||||
},
|
||||
"workflowRun": {
|
||||
"noActiveConnectionToTheServer": "Lost connection to the server",
|
||||
|
@ -1009,5 +1054,15 @@
|
|||
"yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.",
|
||||
"yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.",
|
||||
"gotIt": "Got it"
|
||||
},
|
||||
"workflowPreview": {
|
||||
"showError": {
|
||||
"previewError": {
|
||||
"message": "Unable to preview workflow",
|
||||
"title": "Preview error"
|
||||
},
|
||||
"missingWorkflow": "Missing workflow",
|
||||
"arrayEmpty": "Must have an array of nodes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,15 @@ import {
|
|||
faArrowRight,
|
||||
faAt,
|
||||
faBook,
|
||||
faBoxOpen,
|
||||
faBug,
|
||||
faCalendar,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCode,
|
||||
faCodeBranch,
|
||||
faCog,
|
||||
|
@ -99,10 +102,13 @@ addIcon(faArrowLeft);
|
|||
addIcon(faArrowRight);
|
||||
addIcon(faAt);
|
||||
addIcon(faBook);
|
||||
addIcon(faBoxOpen);
|
||||
addIcon(faBug);
|
||||
addIcon(faCalendar);
|
||||
addIcon(faCheck);
|
||||
addIcon(faCheckCircle);
|
||||
addIcon(faChevronLeft);
|
||||
addIcon(faChevronRight);
|
||||
addIcon(faChevronDown);
|
||||
addIcon(faChevronUp);
|
||||
addIcon(faCode);
|
||||
|
|
|
@ -3,7 +3,9 @@ import {
|
|||
ITelemetrySettings,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { ILogLevel, INodeCreateElement } from "@/Interface";
|
||||
import { ILogLevel, INodeCreateElement, IRootState } from "@/Interface";
|
||||
import { Route } from "vue-router";
|
||||
import { Store } from "vuex";
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
|
@ -35,7 +37,9 @@ interface IUserNodesPanelSession {
|
|||
|
||||
class Telemetry {
|
||||
|
||||
private pageEventQueue: Array<{category?: string, name?: string | null}>;
|
||||
private pageEventQueue: Array<{category: string, route: Route}>;
|
||||
private previousPath: string;
|
||||
private store: Store<IRootState> | null;
|
||||
|
||||
private get telemetry() {
|
||||
// @ts-ignore
|
||||
|
@ -53,17 +57,20 @@ class Telemetry {
|
|||
|
||||
constructor() {
|
||||
this.pageEventQueue = [];
|
||||
this.previousPath = '';
|
||||
this.store = null;
|
||||
}
|
||||
|
||||
init(options: ITelemetrySettings, instanceId: string, logLevel?: ILogLevel) {
|
||||
init(options: ITelemetrySettings, props: {instanceId: string, logLevel?: ILogLevel, store: Store<IRootState>}) {
|
||||
if (options.enabled && !this.telemetry) {
|
||||
if(!options.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logging = logLevel === 'debug' ? { logLevel: 'DEBUG'} : {};
|
||||
this.store = props.store;
|
||||
const logging = props.logLevel === 'debug' ? { logLevel: 'DEBUG'} : {};
|
||||
this.loadTelemetryLibrary(options.config.key, options.config.url, { integrations: { All: false }, loadIntegration: false, ...logging});
|
||||
this.telemetry.identify(instanceId);
|
||||
this.telemetry.identify(props.instanceId);
|
||||
this.flushPageEvents();
|
||||
}
|
||||
}
|
||||
|
@ -74,14 +81,24 @@ class Telemetry {
|
|||
}
|
||||
}
|
||||
|
||||
page(category?: string, name?: string | null) {
|
||||
page(category: string, route: Route) {
|
||||
if (this.telemetry) {
|
||||
this.telemetry.page(category, name);
|
||||
if (route.path === this.previousPath) { // avoid duplicate requests query is changed for example on search page
|
||||
return;
|
||||
}
|
||||
this.previousPath = route.path;
|
||||
|
||||
const pageName = route.name;
|
||||
let properties: {[key: string]: string} = {};
|
||||
if (this.store && route.meta && route.meta.telemetry && typeof route.meta.telemetry.getProperties === 'function') {
|
||||
properties = route.meta.telemetry.getProperties(route, this.store);
|
||||
}
|
||||
this.telemetry.page(category, pageName, properties);
|
||||
}
|
||||
else {
|
||||
this.pageEventQueue.push({
|
||||
category,
|
||||
name,
|
||||
route,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -89,10 +106,8 @@ class Telemetry {
|
|||
flushPageEvents() {
|
||||
const queue = this.pageEventQueue;
|
||||
this.pageEventQueue = [];
|
||||
queue.forEach(({category, name}) => {
|
||||
if (this.telemetry) {
|
||||
this.telemetry.page(category, name);
|
||||
}
|
||||
queue.forEach(({category, route}) => {
|
||||
this.page(category, route);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Router, { Route } from 'vue-router';
|
||||
|
||||
import TemplatesCollectionView from '@/views/TemplatesCollectionView.vue';
|
||||
import MainHeader from '@/components/MainHeader/MainHeader.vue';
|
||||
import MainSidebar from '@/components/MainSidebar.vue';
|
||||
import NodeView from '@/views/NodeView.vue';
|
||||
import TemplatesWorkflowView from '@/views/TemplatesWorkflowView.vue';
|
||||
import TemplatesSearchView from '@/views/TemplatesSearchView.vue';
|
||||
import { Store } from 'vuex';
|
||||
import { IRootState } from './Interface';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
|
@ -11,6 +17,25 @@ export default new Router({
|
|||
// @ts-ignore
|
||||
base: window.BASE_PATH === '/%BASE_PATH%/' ? '/' : window.BASE_PATH,
|
||||
routes: [
|
||||
{
|
||||
path: '/collections/:id',
|
||||
name: 'TemplatesCollectionView',
|
||||
components: {
|
||||
default: TemplatesCollectionView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
templatesEnabled: true,
|
||||
telemetry: {
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
collection_id: route.params.id,
|
||||
wf_template_repo_session_id: store.getters['templates/currentSessionId'],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/execution/:id',
|
||||
name: 'ExecutionById',
|
||||
|
@ -19,6 +44,46 @@ export default new Router({
|
|||
header: MainHeader,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
nodeView: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/templates/:id',
|
||||
name: 'TemplatesWorkflowView',
|
||||
components: {
|
||||
default: TemplatesWorkflowView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
templatesEnabled: true,
|
||||
telemetry: {
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
template_id: route.params.id,
|
||||
wf_template_repo_session_id: store.getters['templates/currentSessionId'],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/templates/',
|
||||
name: 'TemplatesSearchView',
|
||||
components: {
|
||||
default: TemplatesSearchView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
templatesEnabled: true,
|
||||
telemetry: {
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
wf_template_repo_session_id: store.getters['templates/currentSessionId'],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow',
|
||||
|
@ -28,6 +93,9 @@ export default new Router({
|
|||
header: MainHeader,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
nodeView: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow/:name',
|
||||
|
@ -37,10 +105,9 @@ export default new Router({
|
|||
header: MainHeader,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/workflow',
|
||||
meta: {
|
||||
nodeView: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflows/templates/:id',
|
||||
|
@ -50,6 +117,9 @@ export default new Router({
|
|||
header: MainHeader,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
templatesEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflows/demo',
|
||||
|
@ -57,6 +127,12 @@ export default new Router({
|
|||
components: {
|
||||
default: NodeView,
|
||||
},
|
||||
meta: {
|
||||
nodeView: true,
|
||||
telemetry: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ import settings from './modules/settings';
|
|||
import ui from './modules/ui';
|
||||
import workflows from './modules/workflows';
|
||||
import versions from './modules/versions';
|
||||
import templates from './modules/templates';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -94,6 +95,7 @@ const modules = {
|
|||
credentials,
|
||||
tags,
|
||||
settings,
|
||||
templates,
|
||||
workflows,
|
||||
versions,
|
||||
ui,
|
||||
|
|
29
packages/editor-ui/src/views/LoadingView.vue
Normal file
29
packages/editor-ui/src/views/LoadingView.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div :class="$style.wrapper">
|
||||
<div :class="$style.spinner">
|
||||
<n8n-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: var(--color-background-light);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin-bottom: var(--spacing-l);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30%;
|
||||
|
||||
* {
|
||||
color: var(--color-primary);
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -104,7 +104,6 @@
|
|||
@click.stop="clearExecutionData()"
|
||||
/>
|
||||
</div>
|
||||
<Modals />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -115,7 +114,7 @@ import {
|
|||
} from 'jsplumb';
|
||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
||||
import { NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
@ -130,7 +129,6 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
|||
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
|
||||
import DataDisplay from '@/components/DataDisplay.vue';
|
||||
import Modals from '@/components/Modals.vue';
|
||||
import Node from '@/components/Node.vue';
|
||||
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
||||
import NodeSettings from '@/components/NodeSettings.vue';
|
||||
|
@ -197,7 +195,6 @@ export default mixins(
|
|||
name: 'NodeView',
|
||||
components: {
|
||||
DataDisplay,
|
||||
Modals,
|
||||
Node,
|
||||
NodeCreator,
|
||||
NodeSettings,
|
||||
|
@ -242,20 +239,27 @@ export default mixins(
|
|||
async beforeRouteLeave(to, from, next) {
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(
|
||||
const confirmModal = await this.confirmModal(
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.message'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.confirmButtonText'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.cancelButtonText'),
|
||||
true,
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
next(false);
|
||||
} else {
|
||||
// Prevent other popups from displaying
|
||||
|
||||
if (confirmModal === MODAL_CONFIRMED) {
|
||||
const saved = await this.saveCurrentWorkflow({}, false);
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
this.$store.commit('setStateDirty', false);
|
||||
next();
|
||||
} else if (confirmModal === MODAL_CANCEL) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
next();
|
||||
} else if (confirmModal === MODAL_CLOSE) {
|
||||
next(false);
|
||||
}
|
||||
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
@ -347,6 +351,7 @@ export default mixins(
|
|||
};
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resetWorkspace();
|
||||
// Make sure the event listeners get removed again else we
|
||||
// could add up with them registred multiple times
|
||||
document.removeEventListener('keydown', this.keyDown);
|
||||
|
@ -530,7 +535,7 @@ export default mixins(
|
|||
let data: IWorkflowTemplate | undefined;
|
||||
try {
|
||||
this.$externalHooks().run('template.requested', { templateId });
|
||||
data = await this.$store.dispatch('workflows/getWorkflowTemplate', templateId);
|
||||
data = await this.$store.dispatch('templates/getWorkflowTemplate', templateId);
|
||||
|
||||
if (!data) {
|
||||
throw new Error(
|
||||
|
@ -540,22 +545,16 @@ export default mixins(
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
data.workflow.nodes.forEach((node) => {
|
||||
if (!this.$store.getters.nodeType(node.type, node.typeVersion)) {
|
||||
throw new Error(`The ${this.$locale.shortNodeType(node.type)} node is not supported`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('nodeView.couldntImportWorkflow'));
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
this.$router.replace({ name: 'NodeViewNew' });
|
||||
return;
|
||||
}
|
||||
|
||||
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
|
||||
|
||||
this.blankRedirect = true;
|
||||
this.$router.push({ name: 'NodeViewNew', query: { templateId } });
|
||||
this.$router.replace({ name: 'NodeViewNew', query: { templateId } });
|
||||
|
||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||
await this.$store.dispatch('workflows/setNewWorkflowName', data.name);
|
||||
|
@ -955,8 +954,11 @@ export default mixins(
|
|||
},
|
||||
|
||||
cutSelectedNodes () {
|
||||
this.copySelectedNodes(true);
|
||||
this.deleteSelectedNodes();
|
||||
const deleteCopiedNodes = !this.isReadOnly;
|
||||
this.copySelectedNodes(deleteCopiedNodes);
|
||||
if (deleteCopiedNodes) {
|
||||
this.deleteSelectedNodes();
|
||||
}
|
||||
},
|
||||
|
||||
copySelectedNodes (isCut: boolean) {
|
||||
|
@ -1003,6 +1005,9 @@ export default mixins(
|
|||
setZoomLevel (zoomLevel: number) {
|
||||
this.nodeViewScale = zoomLevel; // important for background
|
||||
const element = this.instance.getContainer() as HTMLElement;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
|
||||
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
|
||||
|
@ -1789,14 +1794,19 @@ export default mixins(
|
|||
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(
|
||||
const confirmModal = await this.confirmModal(
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.message'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.confirmButtonText'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.cancelButtonText'),
|
||||
true,
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
|
||||
if (confirmModal === MODAL_CONFIRMED) {
|
||||
const saved = await this.saveCurrentWorkflow();
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
} else if (confirmModal === MODAL_CLOSE) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
@ -2259,11 +2269,13 @@ export default mixins(
|
|||
deleteEveryEndpoint () {
|
||||
// Check as it does not exist on first load
|
||||
if (this.instance) {
|
||||
const nodes = this.$store.getters.allNodes as INodeUi[];
|
||||
// @ts-ignore
|
||||
nodes.forEach((node: INodeUi) => this.instance.destroyDraggable(`${NODE_NAME_PREFIX}${this.$store.getters.getNodeIndex(node.name)}`));
|
||||
try {
|
||||
const nodes = this.$store.getters.allNodes as INodeUi[];
|
||||
// @ts-ignore
|
||||
nodes.forEach((node: INodeUi) => this.instance.destroyDraggable(`${NODE_NAME_PREFIX}${this.$store.getters.getNodeIndex(node.name)}`));
|
||||
|
||||
this.instance.deleteEveryEndpoint();
|
||||
this.instance.deleteEveryEndpoint();
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
matchCredentials(node: INodeUi) {
|
||||
|
@ -2377,7 +2389,11 @@ export default mixins(
|
|||
for (const sourceNode of Object.keys(connections)) {
|
||||
for (const type of Object.keys(connections[sourceNode])) {
|
||||
for (let sourceIndex = 0; sourceIndex < connections[sourceNode][type].length; sourceIndex++) {
|
||||
connections[sourceNode][type][sourceIndex].forEach((
|
||||
const outwardConnections = connections[sourceNode][type][sourceIndex];
|
||||
if (!outwardConnections) {
|
||||
continue;
|
||||
}
|
||||
outwardConnections.forEach((
|
||||
targetData,
|
||||
) => {
|
||||
connectionData = [
|
||||
|
@ -2629,9 +2645,6 @@ export default mixins(
|
|||
const activeWorkflows = await this.restApi().getActiveWorkflows();
|
||||
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
||||
},
|
||||
async loadSettings (): Promise<void> {
|
||||
await this.$store.dispatch('settings/getSettings');
|
||||
},
|
||||
async loadNodeTypes (): Promise<void> {
|
||||
const nodeTypes = await this.restApi().getNodeTypes();
|
||||
this.$store.commit('setNodeTypes', nodeTypes);
|
||||
|
@ -2676,11 +2689,7 @@ export default mixins(
|
|||
this.stopLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
async mounted () {
|
||||
window.addEventListener('message', async (message) => {
|
||||
async onPostMessageReceived(message: MessageEvent) {
|
||||
try {
|
||||
const json = JSON.parse(message.data);
|
||||
if (json && json.command === 'openWorkflow') {
|
||||
|
@ -2699,20 +2708,24 @@ export default mixins(
|
|||
}
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.$on('importWorkflowData', async (data: IDataObject) => {
|
||||
},
|
||||
async onImportWorkflowDataEvent(data: IDataObject) {
|
||||
await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
||||
});
|
||||
|
||||
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||
|
||||
this.$root.$on('importWorkflowUrl', async (data: IDataObject) => {
|
||||
},
|
||||
async onImportWorkflowUrlEvent(data: IDataObject) {
|
||||
const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
|
||||
if (workflowData !== undefined) {
|
||||
await this.importWorkflowData(workflowData);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
this.$titleReset();
|
||||
window.addEventListener('message', this.onPostMessageReceived);
|
||||
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
|
||||
this.startLoading();
|
||||
|
||||
|
@ -2721,7 +2734,6 @@ export default mixins(
|
|||
this.loadCredentials(),
|
||||
this.loadCredentialTypes(),
|
||||
this.loadNodeTypes(),
|
||||
this.loadSettings(),
|
||||
];
|
||||
|
||||
try {
|
||||
|
@ -2770,6 +2782,11 @@ export default mixins(
|
|||
|
||||
destroyed () {
|
||||
this.resetWorkspace();
|
||||
this.$store.commit('setStateDirty', false);
|
||||
window.removeEventListener('message', this.onPostMessageReceived);
|
||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -2841,6 +2858,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
.node-view-background {
|
||||
background-color: var(--color-canvas-background);
|
||||
position: absolute;
|
||||
width: 10000px;
|
||||
height: 10000px;
|
||||
|
|
193
packages/editor-ui/src/views/TemplatesCollectionView.vue
Normal file
193
packages/editor-ui/src/views/TemplatesCollectionView.vue
Normal file
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<TemplatesView :goBackEnabled="true">
|
||||
<template v-slot:header>
|
||||
<div v-if="!notFoundError" :class="$style.wrapper">
|
||||
<div :class="$style.title">
|
||||
<n8n-heading v-if="collection && collection.name" tag="h1" size="2xlarge">
|
||||
{{ collection.name }}
|
||||
</n8n-heading>
|
||||
<n8n-text v-if="collection && collection.name" color="text-base" size="small">
|
||||
{{ $locale.baseText('templates.collection') }}
|
||||
</n8n-text>
|
||||
<n8n-loading :loading="!collection || !collection.name" :rows="2" variant="h1" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.notFound" v-else>
|
||||
<n8n-text color="text-base">{{ $locale.baseText('templates.collectionsNotFound') }}</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!notFoundError" v-slot:content>
|
||||
<div :class="$style.wrapper">
|
||||
<div :class="$style.mainContent">
|
||||
<div :class="$style.markdown" v-if="loading || (collection && collection.description)">
|
||||
<n8n-markdown
|
||||
:content="collection && collection.description"
|
||||
:images="collection && collection.image"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
<TemplateList
|
||||
:infinite-scroll-enabled="false"
|
||||
:loading="loading"
|
||||
:use-workflow-button="true"
|
||||
:workflows="loading ? [] : collectionWorkflows"
|
||||
@useWorkflow="onUseWorkflow"
|
||||
@openTemplate="onOpenTemplate"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.details">
|
||||
<TemplateDetails
|
||||
:block-title="$locale.baseText('template.details.appsInTheCollection')"
|
||||
:loading="loading"
|
||||
:template="collection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TemplatesView>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import TemplateDetails from '@/components/TemplateDetails.vue';
|
||||
import TemplateList from '@/components/TemplateList.vue';
|
||||
import TemplatesView from './TemplatesView.vue';
|
||||
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import {
|
||||
ITemplatesCollection,
|
||||
ITemplatesCollectionFull,
|
||||
ITemplatesWorkflow,
|
||||
ITemplatesWorkflowFull,
|
||||
} from '@/Interface';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { setPageTitle } from '@/components/helpers';
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: 'TemplatesCollectionView',
|
||||
components: {
|
||||
TemplateDetails,
|
||||
TemplateList,
|
||||
TemplatesView,
|
||||
},
|
||||
computed: {
|
||||
collection(): null | ITemplatesCollection | ITemplatesCollectionFull {
|
||||
return this.$store.getters['templates/getCollectionById'](this.collectionId);
|
||||
},
|
||||
collectionId(): string {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
collectionWorkflows(): Array<ITemplatesWorkflow | ITemplatesWorkflowFull> | null {
|
||||
if (!this.collection) {
|
||||
return null;
|
||||
}
|
||||
return this.collection.workflows.map(({ id }) => {
|
||||
return this.$store.getters['templates/getTemplateById'](id) as ITemplatesWorkflow;
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
notFoundError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 50);
|
||||
},
|
||||
onOpenTemplate({event, id}: {event: MouseEvent, id: string}) {
|
||||
this.navigateTo(event, 'TemplatesWorkflowView', id);
|
||||
},
|
||||
onUseWorkflow({event, id}: {event: MouseEvent, id: string}) {
|
||||
this.$telemetry.track('User inserted workflow template', {
|
||||
template_id: id,
|
||||
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
|
||||
source: 'collection',
|
||||
});
|
||||
|
||||
this.navigateTo(event, 'WorkflowTemplate', id);
|
||||
},
|
||||
navigateTo(e: MouseEvent, page: string, id: string) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const route = this.$router.resolve({ name: page, params: { id } });
|
||||
window.open(route.href, '_blank');
|
||||
return;
|
||||
} else {
|
||||
this.$router.push({ name: page, params: { id } });
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
collection(collection: ITemplatesCollection) {
|
||||
if (collection) {
|
||||
setPageTitle(`n8n - Template collection: ${collection.name}`);
|
||||
}
|
||||
else {
|
||||
setPageTitle(`n8n - Templates`);
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.scrollToTop();
|
||||
|
||||
if (this.collection && (this.collection as ITemplatesCollectionFull).full) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('templates/getCollectionById', this.collectionId);
|
||||
} catch (e) {
|
||||
this.notFoundError = true;
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.notFound {
|
||||
padding-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
padding-right: var(--spacing-2xl);
|
||||
margin-bottom: var(--spacing-l);
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
396
packages/editor-ui/src/views/TemplatesSearchView.vue
Normal file
396
packages/editor-ui/src/views/TemplatesSearchView.vue
Normal file
|
@ -0,0 +1,396 @@
|
|||
<template>
|
||||
<TemplatesView>
|
||||
<template v-slot:header>
|
||||
<div :class="$style.wrapper">
|
||||
<div :class="$style.title">
|
||||
<n8n-heading tag="h1" size="2xlarge">
|
||||
{{ $locale.baseText('templates.heading') }}
|
||||
</n8n-heading>
|
||||
</div>
|
||||
<div :class="$style.button">
|
||||
<n8n-button
|
||||
size="medium"
|
||||
:label="$locale.baseText('templates.newButton')"
|
||||
@click="openNewWorkflow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<div :class="$style.contentWrapper">
|
||||
<div :class="$style.filters">
|
||||
<TemplateFilters
|
||||
:categories="allCategories"
|
||||
:sortOnPopulate="areCategoriesPrepopulated"
|
||||
:loading="loadingCategories"
|
||||
:selected="categories"
|
||||
@clear="onCategoryUnselected"
|
||||
@clearAll="onCategoriesCleared"
|
||||
@select="onCategorySelected"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.search">
|
||||
<n8n-input
|
||||
:value="search"
|
||||
:placeholder="$locale.baseText('templates.searchPlaceholder')"
|
||||
@input="onSearchInput"
|
||||
@blur="trackSearch"
|
||||
clearable
|
||||
>
|
||||
<font-awesome-icon icon="search" slot="prefix" />
|
||||
</n8n-input>
|
||||
<div :class="$style.carouselContainer" v-show="collections.length || loadingCollections">
|
||||
<div :class="$style.header">
|
||||
<n8n-heading :bold="true" size="medium" color="text-light">
|
||||
{{ $locale.baseText('templates.collections') }}
|
||||
<span v-if="!loadingCollections" v-text="`(${collections.length})`" />
|
||||
</n8n-heading>
|
||||
</div>
|
||||
|
||||
<CollectionsCarousel
|
||||
:collections="collections"
|
||||
:loading="loadingCollections"
|
||||
@openCollection="onOpenCollection"
|
||||
/>
|
||||
</div>
|
||||
<TemplateList
|
||||
:infinite-scroll-enabled="true"
|
||||
:loading="loadingWorkflows"
|
||||
:total-workflows="totalWorkflows"
|
||||
:workflows="workflows"
|
||||
@loadMore="onLoadMore"
|
||||
@openTemplate="onOpenTemplate"
|
||||
/>
|
||||
<div v-if="endOfSearchMessage" :class="$style.endText">
|
||||
<n8n-text size="medium" color="text-base">
|
||||
<span v-html="endOfSearchMessage" />
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TemplatesView>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import CollectionsCarousel from '@/components/CollectionsCarousel.vue';
|
||||
import TemplateFilters from '@/components/TemplateFilters.vue';
|
||||
import TemplateList from '@/components/TemplateList.vue';
|
||||
import TemplatesView from './TemplatesView.vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { ITemplatesCollection, ITemplatesWorkflow, ITemplatesQuery, ITemplatesCategory } from '@/Interface';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { setPageTitle } from '@/components/helpers';
|
||||
|
||||
interface ISearchEvent {
|
||||
search_string: string;
|
||||
workflow_results_count: number;
|
||||
collection_results_count: number;
|
||||
categories_applied: ITemplatesCategory[];
|
||||
wf_template_repo_session_id: number;
|
||||
}
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: 'TemplatesSearchView',
|
||||
components: {
|
||||
CollectionsCarousel,
|
||||
TemplateFilters,
|
||||
TemplateList,
|
||||
TemplatesView,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('templates', ['allCategories', 'getSearchedWorkflowsTotal', 'getSearchedWorkflows', 'getSearchedCollections']),
|
||||
...mapGetters('settings', ['isTemplatesEndpointReachable']),
|
||||
collections(): ITemplatesCollection[] {
|
||||
return this.getSearchedCollections(this.query) || [];
|
||||
},
|
||||
endOfSearchMessage(): string | null {
|
||||
if (this.loadingWorkflows) {
|
||||
return null;
|
||||
}
|
||||
if (this.workflows.length && this.workflows.length >= this.totalWorkflows) {
|
||||
return this.$locale.baseText('templates.endResult');
|
||||
}
|
||||
if (!this.loadingCollections && this.workflows.length === 0 && this.collections.length === 0) {
|
||||
if (!this.isTemplatesEndpointReachable && this.errorLoadingWorkflows) {
|
||||
return this.$locale.baseText('templates.connectionWarning');
|
||||
}
|
||||
return this.$locale.baseText('templates.noSearchResults');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
query(): ITemplatesQuery {
|
||||
return {
|
||||
categories: this.categories,
|
||||
search: this.search,
|
||||
};
|
||||
},
|
||||
nothingFound(): boolean {
|
||||
return (
|
||||
!this.loadingWorkflows &&
|
||||
!this.loadingCollections &&
|
||||
this.workflows.length === 0 &&
|
||||
this.collections.length === 0
|
||||
);
|
||||
},
|
||||
totalWorkflows(): number {
|
||||
return this.getSearchedWorkflowsTotal(this.query);
|
||||
},
|
||||
workflows(): ITemplatesWorkflow[] {
|
||||
return this.getSearchedWorkflows(this.query) || [];
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
areCategoriesPrepopulated: false,
|
||||
categories: [] as number[],
|
||||
loading: true,
|
||||
loadingCategories: true,
|
||||
loadingCollections: true,
|
||||
loadingWorkflows: true,
|
||||
search: '',
|
||||
searchEventToTrack: null as null | ISearchEvent,
|
||||
errorLoadingWorkflows: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onOpenCollection({event, id}: {event: MouseEvent, id: string}) {
|
||||
this.navigateTo(event, 'TemplatesCollectionView', id);
|
||||
},
|
||||
onOpenTemplate({event, id}: {event: MouseEvent, id: string}) {
|
||||
this.navigateTo(event, 'TemplatesWorkflowView', id);
|
||||
},
|
||||
navigateTo(e: MouseEvent, page: string, id: string) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const route = this.$router.resolve({ name: page, params: { id } });
|
||||
window.open(route.href, '_blank');
|
||||
return;
|
||||
} else {
|
||||
this.$router.push({ name: page, params: { id } });
|
||||
}
|
||||
},
|
||||
updateSearch() {
|
||||
this.updateQueryParam(this.search, this.categories.join(','));
|
||||
this.loadWorkflowsAndCollections(false);
|
||||
},
|
||||
updateSearchTracking(search: string, categories: number[]) {
|
||||
if (!search) {
|
||||
return;
|
||||
}
|
||||
if (this.searchEventToTrack && this.searchEventToTrack.search_string.length > search.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchEventToTrack = {
|
||||
search_string: search,
|
||||
workflow_results_count: this.getSearchedWorkflowsTotal({search, categories}),
|
||||
collection_results_count: this.getSearchedCollections({search, categories}).length,
|
||||
categories_applied: categories.map((categoryId) =>
|
||||
this.$store.getters['templates/getCategoryById'](categoryId),
|
||||
) as ITemplatesCategory[],
|
||||
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
|
||||
};
|
||||
},
|
||||
trackSearch() {
|
||||
if (this.searchEventToTrack) {
|
||||
this.$telemetry.track('User searched workflow templates', this.searchEventToTrack as unknown as IDataObject);
|
||||
this.searchEventToTrack = null;
|
||||
}
|
||||
},
|
||||
openNewWorkflow() {
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
},
|
||||
onSearchInput(search: string) {
|
||||
this.loadingWorkflows = true;
|
||||
this.loadingCollections = true;
|
||||
this.search = search;
|
||||
this.callDebounced('updateSearch', 500, true);
|
||||
|
||||
if (search.length === 0) {
|
||||
this.trackSearch();
|
||||
}
|
||||
},
|
||||
onCategorySelected(selected: number) {
|
||||
this.categories = this.categories.concat(selected);
|
||||
this.updateSearch();
|
||||
this.trackCategories();
|
||||
},
|
||||
onCategoryUnselected(selected: number) {
|
||||
this.categories = this.categories.filter((id) => id !== selected);
|
||||
this.updateSearch();
|
||||
this.trackCategories();
|
||||
},
|
||||
onCategoriesCleared() {
|
||||
this.categories = [];
|
||||
this.updateSearch();
|
||||
},
|
||||
trackCategories() {
|
||||
if (this.categories.length) {
|
||||
this.$telemetry.track('User changed template filters', {
|
||||
search_string: this.search,
|
||||
categories_applied: this.categories.map((categoryId: number) =>
|
||||
this.$store.getters['templates/getCategoryById'](categoryId),
|
||||
),
|
||||
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
|
||||
});
|
||||
}
|
||||
},
|
||||
updateQueryParam(search: string, category: string) {
|
||||
const query = Object.assign({}, this.$route.query);
|
||||
|
||||
if (category.length) {
|
||||
query.categories = category;
|
||||
} else {
|
||||
delete query.categories;
|
||||
}
|
||||
|
||||
if (search.length) {
|
||||
query.search = search;
|
||||
} else {
|
||||
delete query.search;
|
||||
}
|
||||
|
||||
this.$router.replace({ query });
|
||||
},
|
||||
async onLoadMore() {
|
||||
if (this.workflows.length >= this.totalWorkflows) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.loadingWorkflows = true;
|
||||
await this.$store.dispatch('templates/getMoreWorkflows', {
|
||||
categories: this.categories,
|
||||
search: this.search,
|
||||
});
|
||||
} catch (e) {
|
||||
this.$showMessage({
|
||||
title: 'Error',
|
||||
message: 'Could not load more workflows',
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
this.loadingWorkflows = false;
|
||||
}
|
||||
},
|
||||
async loadCategories() {
|
||||
try {
|
||||
await this.$store.dispatch('templates/getCategories');
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
this.loadingCategories = false;
|
||||
},
|
||||
async loadCollections() {
|
||||
try {
|
||||
this.loadingCollections = true;
|
||||
await this.$store.dispatch('templates/getCollections', {
|
||||
categories: this.categories,
|
||||
search: this.search,
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
this.loadingCollections = false;
|
||||
},
|
||||
async loadWorkflows() {
|
||||
try {
|
||||
this.loadingWorkflows = true;
|
||||
await this.$store.dispatch('templates/getWorkflows', {
|
||||
search: this.search,
|
||||
categories: this.categories,
|
||||
});
|
||||
this.errorLoadingWorkflows = false;
|
||||
} catch (e) {
|
||||
this.errorLoadingWorkflows = true;
|
||||
}
|
||||
|
||||
this.loadingWorkflows = false;
|
||||
},
|
||||
async loadWorkflowsAndCollections(initialLoad: boolean) {
|
||||
const search = this.search;
|
||||
const categories = [...this.categories];
|
||||
await Promise.all([this.loadWorkflows(), this.loadCollections()]);
|
||||
if (!initialLoad) {
|
||||
this.updateSearchTracking(search, categories);
|
||||
}
|
||||
},
|
||||
scrollToTop() {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
workflows(newWorkflows) {
|
||||
if (newWorkflows.length === 0) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.trackSearch();
|
||||
next();
|
||||
},
|
||||
async mounted() {
|
||||
setPageTitle('n8n - Templates');
|
||||
this.loadCategories();
|
||||
this.loadWorkflowsAndCollections(true);
|
||||
},
|
||||
async created() {
|
||||
if (this.$route.query.search && typeof this.$route.query.search === 'string') {
|
||||
this.search = this.$route.query.search;
|
||||
}
|
||||
|
||||
if (typeof this.$route.query.categories === 'string' && this.$route.query.categories.length) {
|
||||
this.categories = this.$route.query.categories.split(',').map((categoryId) => parseInt(categoryId, 10));
|
||||
this.areCategoriesPrepopulated = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
width: 200px;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
padding-left: var(--spacing-2xl);
|
||||
|
||||
> * {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
</style>
|
81
packages/editor-ui/src/views/TemplatesView.vue
Normal file
81
packages/editor-ui/src/views/TemplatesView.vue
Normal file
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div :class="$style.template">
|
||||
<div :class="isMenuCollapsed ? $style.menu : $style.expandedMenu"></div>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.goBack" v-if="goBackEnabled">
|
||||
<GoBackButton />
|
||||
</div>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import GoBackButton from '@/components/GoBackButton.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TemplatesView',
|
||||
components: {
|
||||
GoBackButton,
|
||||
},
|
||||
props: {
|
||||
goBackEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isMenuCollapsed(): boolean {
|
||||
return this.$store.getters['ui/sidebarMenuCollapsed'];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.mockMenu {
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.menu {
|
||||
composes: mockMenu;
|
||||
min-width: $--sidebar-width;
|
||||
}
|
||||
|
||||
.expandedMenu {
|
||||
composes: mockMenu;
|
||||
min-width: $--sidebar-expanded-width;
|
||||
}
|
||||
|
||||
.template {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
padding: var(--spacing-3xl) var(--spacing-3xl) var(--spacing-4xl) var(--spacing-3xl);
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: $--breakpoint-md) {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.goBack {
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
194
packages/editor-ui/src/views/TemplatesWorkflowView.vue
Normal file
194
packages/editor-ui/src/views/TemplatesWorkflowView.vue
Normal file
|
@ -0,0 +1,194 @@
|
|||
<template>
|
||||
<TemplatesView :goBackEnabled="true">
|
||||
<template v-slot:header>
|
||||
<div v-if="!notFoundError" :class="$style.wrapper">
|
||||
<div :class="$style.title">
|
||||
<n8n-heading v-if="template && template.name" tag="h1" size="2xlarge">{{
|
||||
template.name
|
||||
}}</n8n-heading>
|
||||
<n8n-text v-if="template && template.name" color="text-base" size="small">
|
||||
{{ $locale.baseText('templates.workflow') }}
|
||||
</n8n-text>
|
||||
<n8n-loading :loading="!template || !template.name" :rows="2" variant="h1" />
|
||||
</div>
|
||||
<div :class="$style.button">
|
||||
<n8n-button
|
||||
v-if="template"
|
||||
:label="$locale.baseText('template.buttons.useThisWorkflowButton')"
|
||||
size="large"
|
||||
@click="navigateTo(template.id, 'WorkflowTemplate', $event)"
|
||||
/>
|
||||
<n8n-loading :loading="!template" :rows="1" variant="button" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.notFound" v-else>
|
||||
<n8n-text color="text-base">{{ $locale.baseText('templates.workflowsNotFound') }}</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!notFoundError" v-slot:content>
|
||||
<div :class="$style.image">
|
||||
<WorkflowPreview
|
||||
v-if="showPreview"
|
||||
:loading="loading"
|
||||
:workflow="template && template.workflow"
|
||||
@close="onHidePreview"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.markdown">
|
||||
<n8n-markdown
|
||||
:content="template && template.description"
|
||||
:images="template && template.image"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.details">
|
||||
<TemplateDetails
|
||||
:block-title="$locale.baseText('template.details.appsInTheWorkflow')"
|
||||
:loading="loading"
|
||||
:template="template"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TemplatesView>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import TemplateDetails from '@/components/TemplateDetails.vue';
|
||||
import TemplatesView from './TemplatesView.vue';
|
||||
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||
|
||||
import { ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { setPageTitle } from '@/components/helpers';
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: 'TemplatesWorkflowView',
|
||||
components: {
|
||||
TemplateDetails,
|
||||
TemplatesView,
|
||||
WorkflowPreview,
|
||||
},
|
||||
computed: {
|
||||
template(): ITemplatesWorkflow | ITemplatesWorkflowFull {
|
||||
return this.$store.getters['templates/getTemplateById'](this.templateId);
|
||||
},
|
||||
templateId() {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
showPreview: true,
|
||||
notFoundError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
navigateTo(id: string, page: string, e: PointerEvent) {
|
||||
if (page === 'WorkflowTemplate') {
|
||||
this.$telemetry.track('User inserted workflow template', {
|
||||
source: 'workflow',
|
||||
template_id: id,
|
||||
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
|
||||
});
|
||||
}
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const route = this.$router.resolve({ name: page, params: { id } });
|
||||
window.open(route.href, '_blank');
|
||||
return;
|
||||
} else {
|
||||
this.$router.push({ name: page, params: { id } });
|
||||
}
|
||||
},
|
||||
onHidePreview() {
|
||||
this.showPreview = false;
|
||||
},
|
||||
scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
template(template: ITemplatesWorkflowFull) {
|
||||
if (template) {
|
||||
setPageTitle(`n8n - Template template: ${template.name}`);
|
||||
}
|
||||
else {
|
||||
setPageTitle(`n8n - Templates`);
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.scrollToTop();
|
||||
|
||||
if (this.template && (this.template as ITemplatesWorkflowFull).full) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('templates/getTemplateById', this.templateId);
|
||||
} catch (e) {
|
||||
this.notFoundError = true;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notFound {
|
||||
padding-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
width: calc(100% - 180px);
|
||||
padding-right: var(--spacing-2xl);
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
@media (max-width: $--breakpoint-xs) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
1
packages/editor-ui/src/vue-agile.d.ts
vendored
Normal file
1
packages/editor-ui/src/vue-agile.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'vue-agile';
|
|
@ -1,4 +1,5 @@
|
|||
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
chainWebpack: config => {
|
||||
|
@ -26,6 +27,7 @@ module.exports = {
|
|||
},
|
||||
plugins: [
|
||||
new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }),
|
||||
new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'),
|
||||
],
|
||||
},
|
||||
css: {
|
||||
|
|
Loading…
Reference in a new issue