mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-27 05:29:42 -08:00
🔀 Merge branch 'master' into 'feature/n8n-public-api''
This commit is contained in:
commit
b434cfb9d6
|
@ -1,3 +1,2 @@
|
||||||
packages/nodes-base
|
|
||||||
packages/editor-ui
|
packages/editor-ui
|
||||||
packages/design-system
|
packages/design-system
|
||||||
|
|
80
.eslintrc.js
80
.eslintrc.js
|
@ -1,4 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
|
@ -21,6 +23,28 @@ module.exports = {
|
||||||
'**/migrations/**',
|
'**/migrations/**',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: './packages/*(cli|core|workflow|node-dev)/**/*.ts',
|
||||||
|
plugins: [
|
||||||
|
/**
|
||||||
|
* Plugin with lint rules for import/export syntax
|
||||||
|
* https://github.com/import-js/eslint-plugin-import
|
||||||
|
*/
|
||||||
|
'eslint-plugin-import',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
|
||||||
|
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
|
||||||
|
*/
|
||||||
|
'@typescript-eslint',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin to report formatting violations as lint violations
|
||||||
|
* https://github.com/prettier/eslint-plugin-prettier
|
||||||
|
*/
|
||||||
|
'eslint-plugin-prettier',
|
||||||
|
],
|
||||||
extends: [
|
extends: [
|
||||||
/**
|
/**
|
||||||
* Config for typescript-eslint recommended ruleset (without type checking)
|
* Config for typescript-eslint recommended ruleset (without type checking)
|
||||||
|
@ -51,27 +75,6 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
'eslint-config-prettier',
|
'eslint-config-prettier',
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
|
||||||
/**
|
|
||||||
* Plugin with lint rules for import/export syntax
|
|
||||||
* https://github.com/import-js/eslint-plugin-import
|
|
||||||
*/
|
|
||||||
'eslint-plugin-import',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
|
|
||||||
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
|
|
||||||
*/
|
|
||||||
'@typescript-eslint',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin to report formatting violations as lint violations
|
|
||||||
* https://github.com/prettier/eslint-plugin-prettier
|
|
||||||
*/
|
|
||||||
'eslint-plugin-prettier',
|
|
||||||
],
|
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
// ******************************************************************
|
// ******************************************************************
|
||||||
// required by prettier plugin
|
// required by prettier plugin
|
||||||
|
@ -122,7 +125,7 @@ module.exports = {
|
||||||
'undefined',
|
'undefined',
|
||||||
],
|
],
|
||||||
|
|
||||||
'no-void': ['error', { 'allowAsStatement': true }],
|
'no-void': ['error', { allowAsStatement: true }],
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// @typescript-eslint
|
// @typescript-eslint
|
||||||
|
@ -186,7 +189,10 @@ module.exports = {
|
||||||
/**
|
/**
|
||||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md
|
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md
|
||||||
*/
|
*/
|
||||||
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
|
'@typescript-eslint/explicit-member-accessibility': [
|
||||||
|
'error',
|
||||||
|
{ accessibility: 'no-public' },
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
|
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
|
||||||
|
@ -363,4 +369,32 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
'import/prefer-default-export': 'off',
|
'import/prefer-default-export': 'off',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./packages/nodes-base/nodes/**/*.ts'],
|
||||||
|
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||||
|
rules: {
|
||||||
|
"n8n-nodes-base/node-param-default-missing": "error",
|
||||||
|
"n8n-nodes-base/node-class-description-missing-subtitle": "error",
|
||||||
|
"n8n-nodes-base/node-class-description-inputs-wrong-trigger-node": "error",
|
||||||
|
"n8n-nodes-base/node-class-description-inputs-wrong-regular-node": "error",
|
||||||
|
"n8n-nodes-base/node-class-description-outputs-wrong": "error",
|
||||||
|
"n8n-nodes-base/node-execute-block-double-assertion-for-items": "error",
|
||||||
|
"n8n-nodes-base/node-param-default-wrong-for-collection": "error",
|
||||||
|
"n8n-nodes-base/node-param-default-wrong-for-boolean": "error",
|
||||||
|
"n8n-nodes-base/node-param-collection-type-unsorted-items": "error",
|
||||||
|
"n8n-nodes-base/node-param-default-wrong-for-fixed-collection": "error",
|
||||||
|
"n8n-nodes-base/node-param-default-wrong-for-multi-options": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-excess-inner-whitespace": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-empty-string": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-comma-separated-hyphen": "error",
|
||||||
|
"n8n-nodes-base/node-param-default-wrong-for-simplify": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-missing-for-return-all": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-missing-final-period": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-missing-for-simplify": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues": "error",
|
||||||
|
"n8n-nodes-base/node-param-description-identical-to-display-name": "error",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
95
CHANGELOG.md
95
CHANGELOG.md
|
@ -1,27 +1,90 @@
|
||||||
# [0.171.0](https://github.com/n8n-io/n8n/compare/n8n@0.170.0...n8n@0.171.0) (2022-04-03)
|
# [0.174.0](https://github.com/n8n-io/n8n/compare/n8n@0.173.1...n8n@0.174.0) (2022-04-25)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **core:** Fix crash on webhook when last node did not return data ([c50d04a](https://github.com/n8n-io/n8n/commit/c50d04af9eb033d82860c336fc7350b5c3f22242))
|
- **core:** Open oauth callback endpoints to be public ([#3168](https://github.com/n8n-io/n8n/issues/3168)) ([01807d6](https://github.com/n8n-io/n8n/commit/01807d613654eb14dad0eb82defa4fab234a1d71))
|
||||||
* **EmailReadImap Node:** Fix issue that crashed process if node was configured wrong ([#3079](https://github.com/n8n-io/n8n/issues/3079)) ([85f15d4](https://github.com/n8n-io/n8n/commit/85f15d49896d876fa3ab84e9fa1846f856851274))
|
- **MicrosoftOneDrive Node:** Fix issue with filenames that contain special characters from uploading ([#3183](https://github.com/n8n-io/n8n/issues/3183)) ([ff26a98](https://github.com/n8n-io/n8n/commit/ff26a987fe244b30f67d6516d84f1f43fed3ec43))
|
||||||
* **Google Tasks Node:** Fix "Show Completed" option and hide title field where not needed ([#2741](https://github.com/n8n-io/n8n/issues/2741)) ([9d703e3](https://github.com/n8n-io/n8n/commit/9d703e366b8e191e0f588469892ebb7b6d03c1d3))
|
- **Slack Node:** Fix credential test ([#3151](https://github.com/n8n-io/n8n/issues/3151)) ([15e6d92](https://github.com/n8n-io/n8n/commit/15e6d9274ad0627dd5ebc30e70757878368042bc))
|
||||||
* **NocoDB Node:** Fix pagination ([#3081](https://github.com/n8n-io/n8n/issues/3081)) ([5f44b0d](https://github.com/n8n-io/n8n/commit/5f44b0dad5254fe9f985b314db8f7d43ab48c712))
|
|
||||||
* **Salesforce Node:** Fix issue that "status" did not get used for Case => Create & Update ([#2212](https://github.com/n8n-io/n8n/issues/2212)) ([1018146](https://github.com/n8n-io/n8n/commit/1018146f21c47eda9f888bd19e92d1106c49267a))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **editor:** Add download button for binary data ([#2992](https://github.com/n8n-io/n8n/issues/2992)) ([13a9db7](https://github.com/n8n-io/n8n/commit/13a9db774576a00d4e3ce1988557654d00067073))
|
- **All AWS Nodes:** Enable support for AWS temporary credentials ([#2587](https://github.com/n8n-io/n8n/issues/2587)) ([ce79e6b](https://github.com/n8n-io/n8n/commit/ce79e6b74f6d94694f16988c8601f7c0639a04b3))
|
||||||
* **Emelia Node:** Add Campaign > Duplicate functionality ([#3000](https://github.com/n8n-io/n8n/issues/3000)) ([0b08be1](https://github.com/n8n-io/n8n/commit/0b08be1c0b2961f235fc2446a36afe3995b4d847)), closes [#3065](https://github.com/n8n-io/n8n/issues/3065) [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075)
|
- **editor:** Add Workflow Stickies (Notes) ([#3154](https://github.com/n8n-io/n8n/issues/3154)) ([31dd01f](https://github.com/n8n-io/n8n/commit/31dd01f9cb7e1b6908a89c3402c78515a6475e61))
|
||||||
* **FTP Node:** Add option to recursively create directories on rename ([#3001](https://github.com/n8n-io/n8n/issues/3001)) ([39a6f41](https://github.com/n8n-io/n8n/commit/39a6f417203b76cfa2c68816c49e86dc7236aba4))
|
- **Google Sheets Node:** Add upsert support ([#2733](https://github.com/n8n-io/n8n/issues/2733)) ([aeb5a12](https://github.com/n8n-io/n8n/commit/aeb5a1234aa610b333525512085fe3b3bd60abef))
|
||||||
* **Mautic Node:** Add credential test and allow trailing slash in host ([#3080](https://github.com/n8n-io/n8n/issues/3080)) ([0a75539](https://github.com/n8n-io/n8n/commit/0a75539cc3d696a8946d7db5ff5842ff54835134))
|
- **Microsoft Teams Node:** Enhancements and cleanup ([#2940](https://github.com/n8n-io/n8n/issues/2940)) ([d446f9e](https://github.com/n8n-io/n8n/commit/d446f9e28176e6ae2875d526cf4b6ac769dc750c))
|
||||||
* **Microsoft Teams Node:** Add chat message support ([#2635](https://github.com/n8n-io/n8n/issues/2635)) ([984f62d](https://github.com/n8n-io/n8n/commit/984f62df9ed92cdf297b3b56300c9f23bf128d2d))
|
- **MongoDB Node:** Allow parsing dates using dot notation ([#2487](https://github.com/n8n-io/n8n/issues/2487)) ([83998a1](https://github.com/n8n-io/n8n/commit/83998a15b0b4bea94aa07984136bdc56523d4f89))
|
||||||
* **Mocean Node:** Add "Delivery Report URL" option and credential tests ([#3075](https://github.com/n8n-io/n8n/issues/3075)) ([c89d2b1](https://github.com/n8n-io/n8n/commit/c89d2b10f2461ff8e90209b8f29c222f9430dba5))
|
|
||||||
* **ServiceNow Node:** Add basicAuth support and fix getColumns loadOptions ([#2712](https://github.com/n8n-io/n8n/issues/2712)) ([2c72584](https://github.com/n8n-io/n8n/commit/2c72584b55521b437baa20ddad7c919807fd9f8f)), closes [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075) [#3000](https://github.com/n8n-io/n8n/issues/3000) [#3065](https://github.com/n8n-io/n8n/issues/3065) [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075) [#3071](https://github.com/n8n-io/n8n/issues/3071) [#3001](https://github.com/n8n-io/n8n/issues/3001) [#2635](https://github.com/n8n-io/n8n/issues/2635) [#3080](https://github.com/n8n-io/n8n/issues/3080) [#3061](https://github.com/n8n-io/n8n/issues/3061) [#3081](https://github.com/n8n-io/n8n/issues/3081) [#2582](https://github.com/n8n-io/n8n/issues/2582) [#2212](https://github.com/n8n-io/n8n/issues/2212)
|
|
||||||
* **Strava Node:** Add "Get Streams" operation ([#2582](https://github.com/n8n-io/n8n/issues/2582)) ([6bbb4df](https://github.com/n8n-io/n8n/commit/6bbb4df05925362404f844a23a695f186d27b72e))
|
|
||||||
|
|
||||||
|
# [0.173.1](https://github.com/n8n-io/n8n/compare/n8n@0.173.0...n8n@0.173.1) (2022-04-19)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Discord Node:** Fix icon name
|
||||||
|
|
||||||
|
# [0.173.0](https://github.com/n8n-io/n8n/compare/n8n@0.172.0...n8n@0.173.0) (2022-04-19)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **core:** Add "rawBody" also for xml requests ([#3143](https://github.com/n8n-io/n8n/issues/3143)) ([5719e44](https://github.com/n8n-io/n8n/commit/5719e44b5999bb84e2fd50c8a469cc8934539747))
|
||||||
|
- **core:** Make email for UM case insensitive ([#3078](https://github.com/n8n-io/n8n/issues/3078)) ([8532b00](https://github.com/n8n-io/n8n/commit/8532b0030dbdeb85b2f74ce078adb44f43a7c4d3))
|
||||||
|
- **Discourse Node:** Fix issue with not all posts getting returned and add credential test ([#3007](https://github.com/n8n-io/n8n/issues/3007)) ([d68b7a4](https://github.com/n8n-io/n8n/commit/d68b7a4cf4b1025ce19e23f6bfc9e423595b6c0b))
|
||||||
|
- **editor:** Fix breaking Drop-downs after removing expressions ([#3094](https://github.com/n8n-io/n8n/issues/3094)) ([17b0cd8](https://github.com/n8n-io/n8n/commit/17b0cd8f765ce262241c827a635e64c189acc0f8))
|
||||||
|
- **Postgres Node:** Fix issue with columns containing spaces ([#2989](https://github.com/n8n-io/n8n/issues/2989)) ([0081d02](https://github.com/n8n-io/n8n/commit/0081d02b979ff5d98c5a834c60d8d8b5e83924ef))
|
||||||
|
- **ui:** Reset text-edit input value when pressing esc key to have matching input values ([#3098](https://github.com/n8n-io/n8n/issues/3098)) ([29fdd77](https://github.com/n8n-io/n8n/commit/29fdd77d7b4ac3bbb9faae73b0932183d48ae9a6))
|
||||||
|
- **ZendeskTrigger Node:** Fix deprecated targets, replaced with webhooks ([#3025](https://github.com/n8n-io/n8n/issues/3025)) ([794ad7c](https://github.com/n8n-io/n8n/commit/794ad7c756c68e0459d8f105acc3bcc1347d1e59))
|
||||||
|
- **Zoho Node:** Fix pagination issue ([#3129](https://github.com/n8n-io/n8n/issues/3129)) ([47bbe98](https://github.com/n8n-io/n8n/commit/47bbe9857b5f3321c9402595041afcb6b96411c4))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Discord Node:** Add additional options ([#2918](https://github.com/n8n-io/n8n/issues/2918)) ([310bffe](https://github.com/n8n-io/n8n/commit/310bffe7137f6baf36b93719c1e5abe8596dd346))
|
||||||
|
- **editor:** Add drag and drop from nodes panel ([#3123](https://github.com/n8n-io/n8n/issues/3123)) ([f566569](https://github.com/n8n-io/n8n/commit/f56656929992b98a3473944fd2a395e05d5c42f0))
|
||||||
|
- **Google Cloud Realtime Database Node:** Make it possible to select region ([#3096](https://github.com/n8n-io/n8n/issues/3096)) ([176538e](https://github.com/n8n-io/n8n/commit/176538e5f21f14ea3e5964dbe905fe4af89faaef))
|
||||||
|
- **GoogleBigQuery Node:** Add support for service account authentication ([#3128](https://github.com/n8n-io/n8n/issues/3128)) ([ac5f357](https://github.com/n8n-io/n8n/commit/ac5f357001b6887d649f65bc32a30e30aa75584b))
|
||||||
|
- **Markdown Node:** Add new node to covert between Markdown <> HTML ([#1728](https://github.com/n8n-io/n8n/issues/1728)) ([5d1ddb0](https://github.com/n8n-io/n8n/commit/5d1ddb0e9b56d999ec4d9278b81262aafceb43a9))
|
||||||
|
- **PagerDuty Node:** Add support for additional details in incidents ([#3140](https://github.com/n8n-io/n8n/issues/3140)) ([6ca7454](https://github.com/n8n-io/n8n/commit/6ca74540782623ac2301550b62f3382e88b8ed83)), closes [#3094](https://github.com/n8n-io/n8n/issues/3094) [#3105](https://github.com/n8n-io/n8n/issues/3105) [#3112](https://github.com/n8n-io/n8n/issues/3112) [#3078](https://github.com/n8n-io/n8n/issues/3078) [#3133](https://github.com/n8n-io/n8n/issues/3133) [#2918](https://github.com/n8n-io/n8n/issues/2918)
|
||||||
|
- **Slack Node:** Add blocks to slack message update ([#2182](https://github.com/n8n-io/n8n/issues/2182)) ([b5b6000](https://github.com/n8n-io/n8n/commit/b5b60008d680cd843a418390d451743fc13cac9c)), closes [#1728](https://github.com/n8n-io/n8n/issues/1728)
|
||||||
|
|
||||||
|
# [0.172.0](https://github.com/n8n-io/n8n/compare/n8n@0.171.1...n8n@0.172.0) (2022-04-11)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Action Network Node:** Fix pagination issue and add credential test ([#3011](https://github.com/n8n-io/n8n/issues/3011)) ([9ef339e](https://github.com/n8n-io/n8n/commit/9ef339e5257e4aa79600554c815cb32fd226753d))
|
||||||
|
- **core:** Set correct timezone in luxon ([#3115](https://github.com/n8n-io/n8n/issues/3115)) ([3763f81](https://github.com/n8n-io/n8n/commit/3763f815bd14dcc45786efb9b97bb85695bbf734))
|
||||||
|
- **editor:** Fix i18n issues ([#3072](https://github.com/n8n-io/n8n/issues/3072)) ([4ae0f5b](https://github.com/n8n-io/n8n/commit/4ae0f5b6fba65bfa8f236657d89358f53e465c69)), closes [#3097](https://github.com/n8n-io/n8n/issues/3097)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **editor:** Refactor Node Output Panel [PR#3097](https://github.com/PR/issues/3097)
|
||||||
|
- **Magento 2 Node:** Add credential tests ([#3086](https://github.com/n8n-io/n8n/issues/3086)) ([a11b00a](https://github.com/n8n-io/n8n/commit/a11b00a0374359f0ba8fe91a1df402f32de61b15))
|
||||||
|
- **PayPal Node:** Add auth test, fix typo and update API URL ([#3084](https://github.com/n8n-io/n8n/issues/3084)) ([c7a037e](https://github.com/n8n-io/n8n/commit/c7a037e9feed94b641e6aab92301c8a647a2934c)), closes [PR#2568](https://github.com/PR/issues/2568)
|
||||||
|
|
||||||
|
## [0.171.1](https://github.com/n8n-io/n8n/compare/n8n@0.171.0...n8n@0.171.1) (2022-04-06)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **core:** Fix issue with current executions not getting displayed ([#3093](https://github.com/n8n-io/n8n/issues/3093)) ([4af5168](https://github.com/n8n-io/n8n/commit/4af5168b3bc92578dc807bab1c11e3d90e151928))
|
||||||
|
- **core:** Fix issue with falsely skip authorizing ([#3087](https://github.com/n8n-io/n8n/issues/3087)) ([358a683](https://github.com/n8n-io/n8n/commit/358a683f381aa8eb7edd4886d6bdfe7ada61ec35))
|
||||||
|
- **WooCommerce Node:** Fix pagination issue with "Get All" operation ([#2529](https://github.com/n8n-io/n8n/issues/2529)) ([c2a5e0d](https://github.com/n8n-io/n8n/commit/c2a5e0d1b6a89cb7397b93bbb0f0be9be0df9c86))
|
||||||
|
|
||||||
|
# [0.171.0](https://github.com/n8n-io/n8n/compare/n8n@0.170.0...n8n@0.171.0) (2022-04-03)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **core:** Fix crash on webhook when last node did not return data ([c50d04a](https://github.com/n8n-io/n8n/commit/c50d04af9eb033d82860c336fc7350b5c3f22242))
|
||||||
|
- **EmailReadImap Node:** Fix issue that crashed process if node was configured wrong ([#3079](https://github.com/n8n-io/n8n/issues/3079)) ([85f15d4](https://github.com/n8n-io/n8n/commit/85f15d49896d876fa3ab84e9fa1846f856851274))
|
||||||
|
- **Google Tasks Node:** Fix "Show Completed" option and hide title field where not needed ([#2741](https://github.com/n8n-io/n8n/issues/2741)) ([9d703e3](https://github.com/n8n-io/n8n/commit/9d703e366b8e191e0f588469892ebb7b6d03c1d3))
|
||||||
|
- **NocoDB Node:** Fix pagination ([#3081](https://github.com/n8n-io/n8n/issues/3081)) ([5f44b0d](https://github.com/n8n-io/n8n/commit/5f44b0dad5254fe9f985b314db8f7d43ab48c712))
|
||||||
|
- **Salesforce Node:** Fix issue that "status" did not get used for Case => Create & Update ([#2212](https://github.com/n8n-io/n8n/issues/2212)) ([1018146](https://github.com/n8n-io/n8n/commit/1018146f21c47eda9f888bd19e92d1106c49267a))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **editor:** Add download button for binary data ([#2992](https://github.com/n8n-io/n8n/issues/2992)) ([13a9db7](https://github.com/n8n-io/n8n/commit/13a9db774576a00d4e3ce1988557654d00067073))
|
||||||
|
- **Emelia Node:** Add Campaign > Duplicate functionality ([#3000](https://github.com/n8n-io/n8n/issues/3000)) ([0b08be1](https://github.com/n8n-io/n8n/commit/0b08be1c0b2961f235fc2446a36afe3995b4d847)), closes [#3065](https://github.com/n8n-io/n8n/issues/3065) [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075)
|
||||||
|
- **FTP Node:** Add option to recursively create directories on rename ([#3001](https://github.com/n8n-io/n8n/issues/3001)) ([39a6f41](https://github.com/n8n-io/n8n/commit/39a6f417203b76cfa2c68816c49e86dc7236aba4))
|
||||||
|
- **Mautic Node:** Add credential test and allow trailing slash in host ([#3080](https://github.com/n8n-io/n8n/issues/3080)) ([0a75539](https://github.com/n8n-io/n8n/commit/0a75539cc3d696a8946d7db5ff5842ff54835134))
|
||||||
|
- **Microsoft Teams Node:** Add chat message support ([#2635](https://github.com/n8n-io/n8n/issues/2635)) ([984f62d](https://github.com/n8n-io/n8n/commit/984f62df9ed92cdf297b3b56300c9f23bf128d2d))
|
||||||
|
- **Mocean Node:** Add "Delivery Report URL" option and credential tests ([#3075](https://github.com/n8n-io/n8n/issues/3075)) ([c89d2b1](https://github.com/n8n-io/n8n/commit/c89d2b10f2461ff8e90209b8f29c222f9430dba5))
|
||||||
|
- **ServiceNow Node:** Add basicAuth support and fix getColumns loadOptions ([#2712](https://github.com/n8n-io/n8n/issues/2712)) ([2c72584](https://github.com/n8n-io/n8n/commit/2c72584b55521b437baa20ddad7c919807fd9f8f)), closes [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075) [#3000](https://github.com/n8n-io/n8n/issues/3000) [#3065](https://github.com/n8n-io/n8n/issues/3065) [#2741](https://github.com/n8n-io/n8n/issues/2741) [#3075](https://github.com/n8n-io/n8n/issues/3075) [#3071](https://github.com/n8n-io/n8n/issues/3071) [#3001](https://github.com/n8n-io/n8n/issues/3001) [#2635](https://github.com/n8n-io/n8n/issues/2635) [#3080](https://github.com/n8n-io/n8n/issues/3080) [#3061](https://github.com/n8n-io/n8n/issues/3061) [#3081](https://github.com/n8n-io/n8n/issues/3081) [#2582](https://github.com/n8n-io/n8n/issues/2582) [#2212](https://github.com/n8n-io/n8n/issues/2212)
|
||||||
|
- **Strava Node:** Add "Get Streams" operation ([#2582](https://github.com/n8n-io/n8n/issues/2582)) ([6bbb4df](https://github.com/n8n-io/n8n/commit/6bbb4df05925362404f844a23a695f186d27b72e))
|
||||||
|
|
||||||
# [0.170.0](https://github.com/n8n-io/n8n/compare/n8n@0.169.0...n8n@0.170.0) (2022-03-27)
|
# [0.170.0](https://github.com/n8n-io/n8n/compare/n8n@0.169.0...n8n@0.170.0) (2022-03-27)
|
||||||
|
|
||||||
|
|
6452
package-lock.json
generated
6452
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -108,8 +108,7 @@ export class Execute extends Command {
|
||||||
if (flags.id) {
|
if (flags.id) {
|
||||||
// Id of workflow is given
|
// Id of workflow is given
|
||||||
workflowId = flags.id;
|
workflowId = flags.id;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
workflowData = await Db.collections.Workflow.findOne(workflowId);
|
||||||
workflowData = await Db.collections.Workflow!.findOne(workflowId);
|
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
console.info(`The workflow with the id "${workflowId}" does not exist.`);
|
console.info(`The workflow with the id "${workflowId}" does not exist.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -297,7 +297,7 @@ export class ExecuteBatch extends Command {
|
||||||
|
|
||||||
let allWorkflows;
|
let allWorkflows;
|
||||||
|
|
||||||
const query = Db.collections.Workflow!.createQueryBuilder('workflows');
|
const query = Db.collections.Workflow.createQueryBuilder('workflows');
|
||||||
|
|
||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
query.andWhere(`workflows.id in (:...ids)`, { ids });
|
query.andWhere(`workflows.id in (:...ids)`, { ids });
|
||||||
|
|
|
@ -119,13 +119,10 @@ export class ExportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const credentials = await Db.collections.Credentials!.find(findQuery);
|
const credentials = await Db.collections.Credentials.find(findQuery);
|
||||||
|
|
||||||
if (flags.decrypted) {
|
if (flags.decrypted) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
|
||||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < credentials.length; i++) {
|
for (let i = 0; i < credentials.length; i++) {
|
||||||
const { name, type, nodesAccess, data } = credentials[i];
|
const { name, type, nodesAccess, data } = credentials[i];
|
||||||
|
|
|
@ -111,7 +111,7 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const workflows = await Db.collections.Workflow!.find(findQuery);
|
const workflows = await Db.collections.Workflow.find(findQuery);
|
||||||
|
|
||||||
if (workflows.length === 0) {
|
if (workflows.length === 0) {
|
||||||
throw new Error('No workflows found with specified filters.');
|
throw new Error('No workflows found with specified filters.');
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-shadow */
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
@ -86,9 +85,6 @@ export class ImportCredentialsCommand extends Command {
|
||||||
await UserSettings.prepareUserSettings();
|
await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
|
||||||
throw new Error('No encryption key found to encrypt the credentials!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
const files = await glob(
|
const files = await glob(
|
||||||
|
@ -150,7 +146,7 @@ export class ImportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initOwnerCredentialRole() {
|
private async initOwnerCredentialRole() {
|
||||||
const ownerCredentialRole = await Db.collections.Role!.findOne({
|
const ownerCredentialRole = await Db.collections.Role.findOne({
|
||||||
where: { name: 'owner', scope: 'credential' },
|
where: { name: 'owner', scope: 'credential' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -180,11 +176,11 @@ export class ImportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getOwner() {
|
private async getOwner() {
|
||||||
const ownerGlobalRole = await Db.collections.Role!.findOne({
|
const ownerGlobalRole = await Db.collections.Role.findOne({
|
||||||
where: { name: 'owner', scope: 'global' },
|
where: { name: 'owner', scope: 'global' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const owner = await Db.collections.User!.findOne({ globalRole: ownerGlobalRole });
|
const owner = await Db.collections.User.findOne({ globalRole: ownerGlobalRole });
|
||||||
|
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`);
|
throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`);
|
||||||
|
@ -194,7 +190,7 @@ export class ImportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAssignee(userId: string) {
|
private async getAssignee(userId: string) {
|
||||||
const user = await Db.collections.User!.findOne(userId);
|
const user = await Db.collections.User.findOne(userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Failed to find user with ID ${userId}`);
|
throw new Error(`Failed to find user with ID ${userId}`);
|
||||||
|
|
|
@ -157,7 +157,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initOwnerWorkflowRole() {
|
private async initOwnerWorkflowRole() {
|
||||||
const ownerWorkflowRole = await Db.collections.Role!.findOne({
|
const ownerWorkflowRole = await Db.collections.Role.findOne({
|
||||||
where: { name: 'owner', scope: 'workflow' },
|
where: { name: 'owner', scope: 'workflow' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -187,11 +187,11 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getOwner() {
|
private async getOwner() {
|
||||||
const ownerGlobalRole = await Db.collections.Role!.findOne({
|
const ownerGlobalRole = await Db.collections.Role.findOne({
|
||||||
where: { name: 'owner', scope: 'global' },
|
where: { name: 'owner', scope: 'global' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const owner = await Db.collections.User!.findOne({ globalRole: ownerGlobalRole });
|
const owner = await Db.collections.User.findOne({ globalRole: ownerGlobalRole });
|
||||||
|
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`);
|
throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`);
|
||||||
|
@ -201,7 +201,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAssignee(userId: string) {
|
private async getAssignee(userId: string) {
|
||||||
const user = await Db.collections.User!.findOne(userId);
|
const user = await Db.collections.User.findOne(userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Failed to find user with ID ${userId}`);
|
throw new Error(`Failed to find user with ID ${userId}`);
|
||||||
|
|
|
@ -42,8 +42,7 @@ export class ListWorkflowCommand extends Command {
|
||||||
findQuery.active = flags.active === 'true';
|
findQuery.active = flags.active === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const workflows = await Db.collections.Workflow.find(findQuery);
|
||||||
const workflows = await Db.collections.Workflow!.find(findQuery);
|
|
||||||
if (flags.onlyId) {
|
if (flags.onlyId) {
|
||||||
workflows.forEach((workflow) => console.log(workflow.id));
|
workflows.forEach((workflow) => console.log(workflow.id));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,7 +33,6 @@ import {
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
import { getLogger } from '../src/Logger';
|
import { getLogger } from '../src/Logger';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '../src/constants';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
|
@ -173,9 +172,6 @@ export class Start extends Command {
|
||||||
// If we don't have a JWT secret set, generate
|
// If we don't have a JWT secret set, generate
|
||||||
// one based and save to config.
|
// one based and save to config.
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (!encryptionKey) {
|
|
||||||
throw new Error('Fatal error setting up user management: no encryption key set.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a key off every other letter from encryption key
|
// For a key off every other letter from encryption key
|
||||||
// CAREFUL: do not change this or it breaks all existing tokens.
|
// CAREFUL: do not change this or it breaks all existing tokens.
|
||||||
|
@ -210,14 +206,10 @@ export class Start extends Command {
|
||||||
// Wait till the database is ready
|
// Wait till the database is ready
|
||||||
await startDbInitPromise;
|
await startDbInitPromise;
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
if (!encryptionKey) {
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings from database and set them to config.
|
// Load settings from database and set them to config.
|
||||||
const databaseSettings = await Db.collections.Settings!.find({ loadOnStartup: true });
|
const databaseSettings = await Db.collections.Settings.find({ loadOnStartup: true });
|
||||||
databaseSettings.forEach((setting) => {
|
databaseSettings.forEach((setting) => {
|
||||||
config.set(setting.key, JSON.parse(setting.value));
|
config.set(setting.key, JSON.parse(setting.value));
|
||||||
});
|
});
|
||||||
|
@ -287,8 +279,8 @@ export class Start extends Command {
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
|
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
|
||||||
if (shouldRunVacuum) {
|
if (shouldRunVacuum) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
await Db.collections.Execution!.query('VACUUM;');
|
await Db.collections.Execution.query('VACUUM;');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,8 +72,7 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
findQuery.active = true;
|
findQuery.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
await Db.collections.Workflow.update(findQuery, updateQuery);
|
||||||
await Db.collections.Workflow!.update(findQuery, updateQuery);
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error updating database. See log messages for details.');
|
console.error('Error updating database. See log messages for details.');
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
|
|
||||||
import Command from '@oclif/command';
|
import Command from '@oclif/command';
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
|
@ -27,33 +26,33 @@ export class Reset extends Command {
|
||||||
try {
|
try {
|
||||||
const owner = await this.getInstanceOwner();
|
const owner = await this.getInstanceOwner();
|
||||||
|
|
||||||
const ownerWorkflowRole = await Db.collections.Role!.findOneOrFail({
|
const ownerWorkflowRole = await Db.collections.Role.findOneOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'workflow',
|
scope: 'workflow',
|
||||||
});
|
});
|
||||||
|
|
||||||
const ownerCredentialRole = await Db.collections.Role!.findOneOrFail({
|
const ownerCredentialRole = await Db.collections.Role.findOneOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'credential',
|
scope: 'credential',
|
||||||
});
|
});
|
||||||
|
|
||||||
await Db.collections.SharedWorkflow!.update(
|
await Db.collections.SharedWorkflow.update(
|
||||||
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
|
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
|
||||||
{ user: owner },
|
{ user: owner },
|
||||||
);
|
);
|
||||||
|
|
||||||
await Db.collections.SharedCredentials!.update(
|
await Db.collections.SharedCredentials.update(
|
||||||
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
|
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
|
||||||
{ user: owner },
|
{ user: owner },
|
||||||
);
|
);
|
||||||
await Db.collections.User!.delete({ id: Not(owner.id) });
|
await Db.collections.User.delete({ id: Not(owner.id) });
|
||||||
await Db.collections.User!.save(Object.assign(owner, this.defaultUserProps));
|
await Db.collections.User.save(Object.assign(owner, this.defaultUserProps));
|
||||||
|
|
||||||
await Db.collections.Settings!.update(
|
await Db.collections.Settings.update(
|
||||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||||
{ value: 'false' },
|
{ value: 'false' },
|
||||||
);
|
);
|
||||||
await Db.collections.Settings!.update(
|
await Db.collections.Settings.update(
|
||||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||||
{ value: 'false' },
|
{ value: 'false' },
|
||||||
);
|
);
|
||||||
|
@ -68,19 +67,19 @@ export class Reset extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getInstanceOwner(): Promise<User> {
|
private async getInstanceOwner(): Promise<User> {
|
||||||
const globalRole = await Db.collections.Role!.findOneOrFail({
|
const globalRole = await Db.collections.Role.findOneOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
});
|
});
|
||||||
|
|
||||||
const owner = await Db.collections.User!.findOne({ globalRole });
|
const owner = await Db.collections.User.findOne({ globalRole });
|
||||||
|
|
||||||
if (owner) return owner;
|
if (owner) return owner;
|
||||||
|
|
||||||
const user = new User();
|
const user = new User();
|
||||||
|
|
||||||
await Db.collections.User!.save(Object.assign(user, { ...this.defaultUserProps, globalRole }));
|
await Db.collections.User.save(Object.assign(user, { ...this.defaultUserProps, globalRole }));
|
||||||
|
|
||||||
return Db.collections.User!.findOneOrFail({ globalRole });
|
return Db.collections.User.findOneOrFail({ globalRole });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ export class Worker extends Command {
|
||||||
|
|
||||||
async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise<IBullJobResponse> {
|
async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise<IBullJobResponse> {
|
||||||
const jobData = job.data as IBullJobData;
|
const jobData = job.data as IBullJobData;
|
||||||
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId);
|
const executionDb = await Db.collections.Execution.findOne(jobData.executionId);
|
||||||
|
|
||||||
if (!executionDb) {
|
if (!executionDb) {
|
||||||
LoggerProxy.error('Worker failed to find execution data in database. Cannot continue.', {
|
LoggerProxy.error('Worker failed to find execution data in database. Cannot continue.', {
|
||||||
|
@ -139,7 +139,7 @@ export class Worker extends Command {
|
||||||
const findOptions = {
|
const findOptions = {
|
||||||
select: ['id', 'staticData'],
|
select: ['id', 'staticData'],
|
||||||
} as FindOneOptions;
|
} as FindOneOptions;
|
||||||
const workflowData = await Db.collections.Workflow!.findOne(
|
const workflowData = await Db.collections.Workflow.findOne(
|
||||||
currentExecutionDb.workflowData.id,
|
currentExecutionDb.workflowData.id,
|
||||||
findOptions,
|
findOptions,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.171.1",
|
"version": "0.174.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -126,10 +126,10 @@
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.112.0",
|
"n8n-core": "~0.115.0",
|
||||||
"n8n-editor-ui": "~0.138.0",
|
"n8n-editor-ui": "~0.141.0",
|
||||||
"n8n-nodes-base": "~0.169.1",
|
"n8n-nodes-base": "~0.172.0",
|
||||||
"n8n-workflow": "~0.94.0",
|
"n8n-workflow": "~0.97.0",
|
||||||
"nodemailer": "^6.7.1",
|
"nodemailer": "^6.7.1",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class ActiveWorkflowRunner {
|
||||||
// NOTE
|
// NOTE
|
||||||
// Here I guess we can have a flag on the workflow table like hasTrigger
|
// Here I guess we can have a flag on the workflow table like hasTrigger
|
||||||
// so intead of pulling all the active wehhooks just pull the actives that have a trigger
|
// so intead of pulling all the active wehhooks just pull the actives that have a trigger
|
||||||
const workflowsData: IWorkflowDb[] = (await Db.collections.Workflow!.find({
|
const workflowsData: IWorkflowDb[] = (await Db.collections.Workflow.find({
|
||||||
where: { active: true },
|
where: { active: true },
|
||||||
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
||||||
})) as IWorkflowDb[];
|
})) as IWorkflowDb[];
|
||||||
|
@ -256,7 +256,7 @@ export class ActiveWorkflowRunner {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId, {
|
const workflowData = await Db.collections.Workflow.findOne(webhook.workflowId, {
|
||||||
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
||||||
});
|
});
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
|
@ -332,7 +332,7 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async getWebhookMethods(path: string): Promise<string[]> {
|
async getWebhookMethods(path: string): Promise<string[]> {
|
||||||
const webhooks = (await Db.collections.Webhook?.find({ webhookPath: path })) as IWebhookDb[];
|
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path });
|
||||||
|
|
||||||
// Gather all request methods in string array
|
// Gather all request methods in string array
|
||||||
const webhookMethods: string[] = webhooks.map((webhook) => webhook.method);
|
const webhookMethods: string[] = webhooks.map((webhook) => webhook.method);
|
||||||
|
@ -349,12 +349,12 @@ export class ActiveWorkflowRunner {
|
||||||
let activeWorkflows: WorkflowEntity[] = [];
|
let activeWorkflows: WorkflowEntity[] = [];
|
||||||
|
|
||||||
if (!user || user.globalRole.name === 'owner') {
|
if (!user || user.globalRole.name === 'owner') {
|
||||||
activeWorkflows = await Db.collections.Workflow!.find({
|
activeWorkflows = await Db.collections.Workflow.find({
|
||||||
select: ['id'],
|
select: ['id'],
|
||||||
where: { active: true },
|
where: { active: true },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shared = await Db.collections.SharedWorkflow!.find({
|
const shared = await Db.collections.SharedWorkflow.find({
|
||||||
relations: ['workflow'],
|
relations: ['workflow'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user,
|
user,
|
||||||
|
@ -379,7 +379,7 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async isActive(id: string): Promise<boolean> {
|
async isActive(id: string): Promise<boolean> {
|
||||||
const workflow = await Db.collections.Workflow!.findOne(id);
|
const workflow = await Db.collections.Workflow.findOne(id);
|
||||||
return !!workflow?.active;
|
return !!workflow?.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,7 +512,7 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async removeWorkflowWebhooks(workflowId: string): Promise<void> {
|
async removeWorkflowWebhooks(workflowId: string): Promise<void> {
|
||||||
const workflowData = await Db.collections.Workflow!.findOne(workflowId, {
|
const workflowData = await Db.collections.Workflow.findOne(workflowId, {
|
||||||
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
||||||
});
|
});
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
|
@ -715,7 +715,7 @@ export class ActiveWorkflowRunner {
|
||||||
let workflowInstance: Workflow;
|
let workflowInstance: Workflow;
|
||||||
try {
|
try {
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
workflowData = (await Db.collections.Workflow!.findOne(workflowId, {
|
workflowData = (await Db.collections.Workflow.findOne(workflowId, {
|
||||||
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
|
||||||
})) as IWorkflowDb;
|
})) as IWorkflowDb;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
|
||||||
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { get } from 'lodash';
|
||||||
import { NodeVersionedType } from 'n8n-nodes-base';
|
import { NodeVersionedType } from 'n8n-nodes-base';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -235,13 +236,11 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = userId
|
const credential = userId
|
||||||
? await Db.collections
|
? await Db.collections.SharedCredentials.findOneOrFail({
|
||||||
.SharedCredentials!.findOneOrFail({
|
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: { credentials: { id: nodeCredential.id, type }, user: { id: userId } },
|
where: { credentials: { id: nodeCredential.id, type }, user: { id: userId } },
|
||||||
})
|
}).then((shared) => shared.credentials)
|
||||||
.then((shared) => shared.credentials)
|
: await Db.collections.Credentials.findOneOrFail({ id: nodeCredential.id, type });
|
||||||
: await Db.collections.Credentials!.findOneOrFail({ id: nodeCredential.id, type });
|
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -425,7 +424,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
|
|
||||||
if (Db.collections.Credentials === null) {
|
if (!Db.isInitialized) {
|
||||||
// The first time executeWorkflow gets called the Database has
|
// The first time executeWorkflow gets called the Database has
|
||||||
// to get initialized first
|
// to get initialized first
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
@ -445,7 +444,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
|
await Db.collections.Credentials.update(findQuery, newCredentialsData);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentialTestFunction(
|
getCredentialTestFunction(
|
||||||
|
@ -635,8 +634,10 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
mode,
|
mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let response: INodeExecutionData[][] | null | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await routingNode.runNode(
|
response = await routingNode.runNode(
|
||||||
inputData,
|
inputData,
|
||||||
runIndex,
|
runIndex,
|
||||||
nodeTypeCopy,
|
nodeTypeCopy,
|
||||||
|
@ -685,6 +686,24 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credentialTestFunction.testRequest.rules &&
|
||||||
|
Array.isArray(credentialTestFunction.testRequest.rules)
|
||||||
|
) {
|
||||||
|
// Special testing rules are defined so check all in order
|
||||||
|
for (const rule of credentialTestFunction.testRequest.rules) {
|
||||||
|
if (rule.type === 'responseSuccessBody') {
|
||||||
|
const responseData = response![0][0].json;
|
||||||
|
if (get(responseData, rule.properties.key) === rule.properties.value) {
|
||||||
|
return {
|
||||||
|
status: 'Error',
|
||||||
|
message: rule.properties.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
message: 'Connection successful!',
|
message: 'Connection successful!',
|
||||||
|
@ -721,8 +740,7 @@ export async function getCredentialForUser(
|
||||||
credentialId: string,
|
credentialId: string,
|
||||||
user: User,
|
user: User,
|
||||||
): Promise<ICredentialsDb | null> {
|
): Promise<ICredentialsDb | null> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const sharedCredential = await Db.collections.SharedCredentials.findOne({
|
||||||
const sharedCredential = await Db.collections.SharedCredentials!.findOne({
|
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user,
|
user,
|
||||||
|
@ -735,3 +753,13 @@ export async function getCredentialForUser(
|
||||||
|
|
||||||
return sharedCredential.credentials as ICredentialsDb;
|
return sharedCredential.credentials as ICredentialsDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a credential without user check
|
||||||
|
*/
|
||||||
|
export async function getCredentialWithoutUser(
|
||||||
|
credentialId: string,
|
||||||
|
): Promise<ICredentialsDb | undefined> {
|
||||||
|
const credential = await Db.collections.Credentials.findOne(credentialId);
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable import/no-mutable-exports */
|
||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
@ -28,18 +29,8 @@ import { postgresMigrations } from './databases/postgresdb/migrations';
|
||||||
import { mysqlMigrations } from './databases/mysqldb/migrations';
|
import { mysqlMigrations } from './databases/mysqldb/migrations';
|
||||||
import { sqliteMigrations } from './databases/sqlite/migrations';
|
import { sqliteMigrations } from './databases/sqlite/migrations';
|
||||||
|
|
||||||
export const collections: IDatabaseCollections = {
|
export let isInitialized = false;
|
||||||
Credentials: null,
|
export const collections = {} as IDatabaseCollections;
|
||||||
Execution: null,
|
|
||||||
Workflow: null,
|
|
||||||
Webhook: null,
|
|
||||||
Tag: null,
|
|
||||||
Role: null,
|
|
||||||
User: null,
|
|
||||||
SharedCredentials: null,
|
|
||||||
SharedWorkflow: null,
|
|
||||||
Settings: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
let connection: Connection;
|
let connection: Connection;
|
||||||
|
|
||||||
|
@ -202,5 +193,7 @@ export async function init(
|
||||||
collections.SharedWorkflow = linkRepository(entities.SharedWorkflow);
|
collections.SharedWorkflow = linkRepository(entities.SharedWorkflow);
|
||||||
collections.Settings = linkRepository(entities.Settings);
|
collections.Settings = linkRepository(entities.Settings);
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
return collections;
|
return collections;
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,8 +165,8 @@ export async function generateUniqueName(
|
||||||
|
|
||||||
const found: Array<WorkflowEntity | ICredentialsDb> =
|
const found: Array<WorkflowEntity | ICredentialsDb> =
|
||||||
entityType === 'workflow'
|
entityType === 'workflow'
|
||||||
? await Db.collections.Workflow!.find(findConditions)
|
? await Db.collections.Workflow.find(findConditions)
|
||||||
: await Db.collections.Credentials!.find(findConditions);
|
: await Db.collections.Credentials.find(findConditions);
|
||||||
|
|
||||||
// name is unique
|
// name is unique
|
||||||
if (found.length === 0) {
|
if (found.length === 0) {
|
||||||
|
|
|
@ -72,16 +72,16 @@ export interface ICredentialsOverwrite {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDatabaseCollections {
|
export interface IDatabaseCollections {
|
||||||
Credentials: Repository<ICredentialsDb> | null;
|
Credentials: Repository<ICredentialsDb>;
|
||||||
Execution: Repository<IExecutionFlattedDb> | null;
|
Execution: Repository<IExecutionFlattedDb>;
|
||||||
Workflow: Repository<WorkflowEntity> | null;
|
Workflow: Repository<WorkflowEntity>;
|
||||||
Webhook: Repository<IWebhookDb> | null;
|
Webhook: Repository<IWebhookDb>;
|
||||||
Tag: Repository<TagEntity> | null;
|
Tag: Repository<TagEntity>;
|
||||||
Role: Repository<Role> | null;
|
Role: Repository<Role>;
|
||||||
User: Repository<User> | null;
|
User: Repository<User>;
|
||||||
SharedCredentials: Repository<SharedCredentials> | null;
|
SharedCredentials: Repository<SharedCredentials>;
|
||||||
SharedWorkflow: Repository<SharedWorkflow> | null;
|
SharedWorkflow: Repository<SharedWorkflow>;
|
||||||
Settings: Repository<Settings> | null;
|
Settings: Repository<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWebhookDb {
|
export interface IWebhookDb {
|
||||||
|
|
|
@ -84,11 +84,18 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
||||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
|
||||||
|
const notesCount = Object.keys(nodeGraph.notes).length;
|
||||||
|
const overlappingCount = Object.values(nodeGraph.notes).filter(
|
||||||
|
(note) => note.overlapping,
|
||||||
|
).length;
|
||||||
|
|
||||||
return this.telemetry.track('User saved workflow', {
|
return this.telemetry.track('User saved workflow', {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
node_graph: nodeGraph,
|
node_graph: nodeGraph,
|
||||||
node_graph_string: JSON.stringify(nodeGraph),
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
|
notes_count_overlapping: overlappingCount,
|
||||||
|
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||||
version_cli: this.versionCli,
|
version_cli: this.versionCli,
|
||||||
num_tags: workflow.tags?.length ?? 0,
|
num_tags: workflow.tags?.length ?? 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync, promises } from 'fs';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import _, { cloneDeep } from 'lodash';
|
import _, { cloneDeep } from 'lodash';
|
||||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
|
@ -137,6 +137,8 @@ import {
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
|
getCredentialForUser,
|
||||||
|
getCredentialWithoutUser,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
@ -641,6 +643,10 @@ class App {
|
||||||
normalizeTags: true, // Transform tags to lowercase
|
normalizeTags: true, // Transform tags to lowercase
|
||||||
explicitArray: false, // Only put properties in array if length > 1
|
explicitArray: false, // Only put properties in array if length > 1
|
||||||
},
|
},
|
||||||
|
verify: (req: express.Request, res: any, buf: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -699,7 +705,7 @@ class App {
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (Db.collections.Workflow === null) {
|
if (!Db.isInitialized) {
|
||||||
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
return ResponseHelper.sendErrorResponse(res, error);
|
||||||
}
|
}
|
||||||
|
@ -1531,10 +1537,17 @@ class App {
|
||||||
async (req: express.Request, res: express.Response): Promise<object | void> => {
|
async (req: express.Request, res: express.Response): Promise<object | void> => {
|
||||||
const packagesPath = pathJoin(__dirname, '..', '..', '..');
|
const packagesPath = pathJoin(__dirname, '..', '..', '..');
|
||||||
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
|
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promises.access(`${headersPath}.js`);
|
||||||
|
} catch (_) {
|
||||||
|
return; // no headers available
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return require(headersPath);
|
return require(headersPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send('Failed to find headers file');
|
res.status(500).send('Failed to load headers file');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1724,13 +1737,11 @@ class App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
if (!encryptionKey) {
|
try {
|
||||||
throw new ResponseHelper.ResponseError(
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
} catch (error) {
|
||||||
undefined,
|
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
|
@ -1841,18 +1852,18 @@ class App {
|
||||||
LoggerProxy.error(
|
LoggerProxy.error(
|
||||||
'OAuth1 callback failed because of insufficient parameters received',
|
'OAuth1 callback failed because of insufficient parameters received',
|
||||||
{
|
{
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId,
|
credentialId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = await getCredentialForUser(credentialId, req.user);
|
const credential = await getCredentialWithoutUser(credentialId);
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
|
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId,
|
credentialId,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
const errorResponse = new ResponseHelper.ResponseError(
|
||||||
|
@ -1863,15 +1874,11 @@ class App {
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
} catch (error) {
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||||
undefined,
|
|
||||||
503,
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
|
@ -1906,7 +1913,7 @@ class App {
|
||||||
oauthToken = await requestPromise(options);
|
oauthToken = await requestPromise(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerProxy.error('Unable to fetch tokens for OAuth1 callback', {
|
LoggerProxy.error('Unable to fetch tokens for OAuth1 callback', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId,
|
credentialId,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
const errorResponse = new ResponseHelper.ResponseError(
|
||||||
|
@ -1936,13 +1943,13 @@ class App {
|
||||||
await Db.collections.Credentials!.update(credentialId, newCredentialsData);
|
await Db.collections.Credentials!.update(credentialId, newCredentialsData);
|
||||||
|
|
||||||
LoggerProxy.verbose('OAuth1 callback successful for new credential', {
|
LoggerProxy.verbose('OAuth1 callback successful for new credential', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId,
|
credentialId,
|
||||||
});
|
});
|
||||||
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
|
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
|
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId: req.query.cid,
|
credentialId: req.query.cid,
|
||||||
});
|
});
|
||||||
// Error response
|
// Error response
|
||||||
|
@ -1983,13 +1990,11 @@ class App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
if (!encryptionKey) {
|
try {
|
||||||
throw new ResponseHelper.ResponseError(
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
} catch (error) {
|
||||||
undefined,
|
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
|
@ -2109,11 +2114,11 @@ class App {
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = await getCredentialForUser(state.cid, req.user);
|
const credential = await getCredentialWithoutUser(state.cid);
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
const errorResponse = new ResponseHelper.ResponseError(
|
||||||
|
@ -2124,15 +2129,11 @@ class App {
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
} catch (error) {
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||||
undefined,
|
|
||||||
503,
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
|
@ -2158,7 +2159,7 @@ class App {
|
||||||
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
||||||
) {
|
) {
|
||||||
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
const errorResponse = new ResponseHelper.ResponseError(
|
||||||
|
@ -2209,7 +2210,7 @@ class App {
|
||||||
|
|
||||||
if (oauthToken === undefined) {
|
if (oauthToken === undefined) {
|
||||||
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.ResponseError(
|
const errorResponse = new ResponseHelper.ResponseError(
|
||||||
|
@ -2243,7 +2244,7 @@ class App {
|
||||||
// Save the credentials in DB
|
// Save the credentials in DB
|
||||||
await Db.collections.Credentials!.update(state.cid, newCredentialsData);
|
await Db.collections.Credentials!.update(state.cid, newCredentialsData);
|
||||||
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
|
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
|
||||||
userId: req.user.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as config from '../../config';
|
||||||
import { getWebhookBaseUrl } from '../WebhookHelpers';
|
import { getWebhookBaseUrl } from '../WebhookHelpers';
|
||||||
|
|
||||||
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
|
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
|
||||||
const sharedWorkflow = await Db.collections.SharedWorkflow!.findOneOrFail({
|
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
|
||||||
where: { workflow: { id: workflowId } },
|
where: { workflow: { id: workflowId } },
|
||||||
relations: ['user', 'user.globalRole'],
|
relations: ['user', 'user.globalRole'],
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ export function isEmailSetUp(): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInstanceOwnerRole(): Promise<Role> {
|
async function getInstanceOwnerRole(): Promise<Role> {
|
||||||
const ownerRole = await Db.collections.Role!.findOneOrFail({
|
const ownerRole = await Db.collections.Role.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
|
@ -45,7 +45,7 @@ async function getInstanceOwnerRole(): Promise<Role> {
|
||||||
export async function getInstanceOwner(): Promise<User> {
|
export async function getInstanceOwner(): Promise<User> {
|
||||||
const ownerRole = await getInstanceOwnerRole();
|
const ownerRole = await getInstanceOwnerRole();
|
||||||
|
|
||||||
const owner = await Db.collections.User!.findOneOrFail({
|
const owner = await Db.collections.User.findOneOrFail({
|
||||||
relations: ['globalRole'],
|
relations: ['globalRole'],
|
||||||
where: {
|
where: {
|
||||||
globalRole: ownerRole,
|
globalRole: ownerRole,
|
||||||
|
@ -121,7 +121,7 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserById(userId: string): Promise<User> {
|
export async function getUserById(userId: string): Promise<User> {
|
||||||
const user = await Db.collections.User!.findOneOrFail(userId, {
|
const user = await Db.collections.User.findOneOrFail(userId, {
|
||||||
relations: ['globalRole'],
|
relations: ['globalRole'],
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
|
@ -174,7 +174,7 @@ export async function checkPermissionsForExecution(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for the user's permission to all used credentials
|
// Check for the user's permission to all used credentials
|
||||||
const credentialCount = await Db.collections.SharedCredentials!.count({
|
const credentialCount = await Db.collections.SharedCredentials.count({
|
||||||
where: {
|
where: {
|
||||||
user: { id: userId },
|
user: { id: userId },
|
||||||
credentials: In(ids),
|
credentials: In(ids),
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function issueJWT(user: User): JwtToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
|
export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
|
||||||
const user = await Db.collections.User!.findOne(jwtPayload.id, {
|
const user = await Db.collections.User.findOne(jwtPayload.id, {
|
||||||
relations: ['globalRole'],
|
relations: ['globalRole'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
import { NodeMailer } from './NodeMailer';
|
import { NodeMailer } from './NodeMailer';
|
||||||
|
|
||||||
|
// TODO: make function fully async (remove sync functions)
|
||||||
async function getTemplate(configKeyName: string, defaultFilename: string) {
|
async function getTemplate(configKeyName: string, defaultFilename: string) {
|
||||||
const templateOverride = (await GenericHelpers.getConfigValue(
|
const templateOverride = (await GenericHelpers.getConfigValue(
|
||||||
`userManagement.emails.templates.${configKeyName}`,
|
`userManagement.emails.templates.${configKeyName}`,
|
||||||
|
@ -60,7 +61,6 @@ export class UserManagementMailer {
|
||||||
let template = await getTemplate('invite', 'invite.html');
|
let template = await getTemplate('invite', 'invite.html');
|
||||||
template = replaceStrings(template, inviteEmailData);
|
template = replaceStrings(template, inviteEmailData);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const result = await this.mailer?.sendMail({
|
const result = await this.mailer?.sendMail({
|
||||||
emailRecipients: inviteEmailData.email,
|
emailRecipients: inviteEmailData.email,
|
||||||
subject: 'You have been invited to n8n',
|
subject: 'You have been invited to n8n',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
@ -32,7 +31,7 @@ export function authenticationMethods(this: N8nApp): void {
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
try {
|
try {
|
||||||
user = await Db.collections.User!.findOne(
|
user = await Db.collections.User.findOne(
|
||||||
{
|
{
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
},
|
},
|
||||||
|
@ -91,7 +90,7 @@ export function authenticationMethods(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
user = await Db.collections.User!.findOneOrFail({ relations: ['globalRole'] });
|
user = await Db.collections.User.findOneOrFail({ relations: ['globalRole'] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No users found in database - did you wipe the users table? Create at least one user.',
|
'No users found in database - did you wipe the users table? Create at least one user.',
|
||||||
|
|
|
@ -66,6 +66,8 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
|
||||||
req.url.startsWith(`/${restEndpoint}/forgot-password`) ||
|
req.url.startsWith(`/${restEndpoint}/forgot-password`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/resolve-password-token`) ||
|
req.url.startsWith(`/${restEndpoint}/resolve-password-token`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
||||||
isAuthExcluded(req.url, ignoredEndpoints)
|
isAuthExcluded(req.url, ignoredEndpoints)
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
@ -53,7 +52,7 @@ export function meNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
await validateEntity(newUser);
|
await validateEntity(newUser);
|
||||||
|
|
||||||
const user = await Db.collections.User!.save(newUser);
|
const user = await Db.collections.User.save(newUser);
|
||||||
|
|
||||||
Logger.info('User updated successfully', { userId: user.id });
|
Logger.info('User updated successfully', { userId: user.id });
|
||||||
|
|
||||||
|
@ -99,7 +98,7 @@ export function meNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
req.user.password = await hashPassword(validPassword);
|
req.user.password = await hashPassword(validPassword);
|
||||||
|
|
||||||
const user = await Db.collections.User!.save(req.user);
|
const user = await Db.collections.User.save(req.user);
|
||||||
Logger.info('Password updated successfully', { userId: user.id });
|
Logger.info('Password updated successfully', { userId: user.id });
|
||||||
|
|
||||||
await issueCookie(res, user);
|
await issueCookie(res, user);
|
||||||
|
@ -135,7 +134,7 @@ export function meNamespace(this: N8nApp): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Db.collections.User!.save({
|
await Db.collections.User.save({
|
||||||
id: req.user.id,
|
id: req.user.id,
|
||||||
personalizationAnswers,
|
personalizationAnswers,
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function ownerNamespace(this: N8nApp): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let owner = await Db.collections.User!.findOne(userId, {
|
let owner = await Db.collections.User.findOne(userId, {
|
||||||
relations: ['globalRole'],
|
relations: ['globalRole'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -78,11 +78,11 @@ export function ownerNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
await validateEntity(owner);
|
await validateEntity(owner);
|
||||||
|
|
||||||
owner = await Db.collections.User!.save(owner);
|
owner = await Db.collections.User.save(owner);
|
||||||
|
|
||||||
Logger.info('Owner was set up successfully', { userId: req.user.id });
|
Logger.info('Owner was set up successfully', { userId: req.user.id });
|
||||||
|
|
||||||
await Db.collections.Settings!.update(
|
await Db.collections.Settings.update(
|
||||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||||
{ value: JSON.stringify(true) },
|
{ value: JSON.stringify(true) },
|
||||||
);
|
);
|
||||||
|
@ -108,7 +108,7 @@ export function ownerNamespace(this: N8nApp): void {
|
||||||
`/${this.restEndpoint}/owner/skip-setup`,
|
`/${this.restEndpoint}/owner/skip-setup`,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
ResponseHelper.send(async (_req: AuthenticatedRequest, _res: express.Response) => {
|
ResponseHelper.send(async (_req: AuthenticatedRequest, _res: express.Response) => {
|
||||||
await Db.collections.Settings!.update(
|
await Db.collections.Settings.update(
|
||||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||||
{ value: JSON.stringify(true) },
|
{ value: JSON.stringify(true) },
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,7 +53,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// User should just be able to reset password if one is already present
|
// User should just be able to reset password if one is already present
|
||||||
const user = await Db.collections.User!.findOne({ email, password: Not(IsNull()) });
|
const user = await Db.collections.User.findOne({ email, password: Not(IsNull()) });
|
||||||
|
|
||||||
if (!user || !user.password) {
|
if (!user || !user.password) {
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
|
@ -69,7 +69,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200;
|
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200;
|
||||||
|
|
||||||
await Db.collections.User!.update(id, { resetPasswordToken, resetPasswordTokenExpiration });
|
await Db.collections.User.update(id, { resetPasswordToken, resetPasswordTokenExpiration });
|
||||||
|
|
||||||
const baseUrl = getInstanceBaseUrl();
|
const baseUrl = getInstanceBaseUrl();
|
||||||
const url = new URL(`${baseUrl}/change-password`);
|
const url = new URL(`${baseUrl}/change-password`);
|
||||||
|
@ -134,7 +134,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
// Timestamp is saved in seconds
|
// Timestamp is saved in seconds
|
||||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const user = await Db.collections.User!.findOne({
|
const user = await Db.collections.User.findOne({
|
||||||
id,
|
id,
|
||||||
resetPasswordToken,
|
resetPasswordToken,
|
||||||
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
||||||
|
@ -187,7 +187,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
// Timestamp is saved in seconds
|
// Timestamp is saved in seconds
|
||||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const user = await Db.collections.User!.findOne({
|
const user = await Db.collections.User.findOne({
|
||||||
id: userId,
|
id: userId,
|
||||||
resetPasswordToken,
|
resetPasswordToken,
|
||||||
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
||||||
|
@ -204,7 +204,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
throw new ResponseHelper.ResponseError('', undefined, 404);
|
throw new ResponseHelper.ResponseError('', undefined, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Db.collections.User!.update(userId, {
|
await Db.collections.User.update(userId, {
|
||||||
password: await hashPassword(validPassword),
|
password: await hashPassword(validPassword),
|
||||||
resetPasswordToken: null,
|
resetPasswordToken: null,
|
||||||
resetPasswordTokenExpiration: null,
|
resetPasswordTokenExpiration: null,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable no-restricted-syntax */
|
/* eslint-disable no-restricted-syntax */
|
||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
|
@ -106,10 +105,10 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
400,
|
400,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
createUsers[invite.email] = null;
|
createUsers[invite.email.toLowerCase()] = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const role = await Db.collections.Role!.findOne({ scope: 'global', name: 'member' });
|
const role = await Db.collections.Role.findOne({ scope: 'global', name: 'member' });
|
||||||
|
|
||||||
if (!role) {
|
if (!role) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
|
@ -123,7 +122,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove/exclude existing users from creation
|
// remove/exclude existing users from creation
|
||||||
const existingUsers = await Db.collections.User!.find({
|
const existingUsers = await Db.collections.User.find({
|
||||||
where: { email: In(Object.keys(createUsers)) },
|
where: { email: In(Object.keys(createUsers)) },
|
||||||
});
|
});
|
||||||
existingUsers.forEach((user) => {
|
existingUsers.forEach((user) => {
|
||||||
|
@ -191,6 +190,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
};
|
};
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
void InternalHooksManager.getInstance().onUserTransactionalEmail({
|
void InternalHooksManager.getInstance().onUserTransactionalEmail({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
user_id: id!,
|
user_id: id!,
|
||||||
message_type: 'New user invite',
|
message_type: 'New user invite',
|
||||||
});
|
});
|
||||||
|
@ -250,7 +250,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) } });
|
const users = await Db.collections.User.find({ where: { id: In([inviterId, inviteeId]) } });
|
||||||
|
|
||||||
if (users.length !== 2) {
|
if (users.length !== 2) {
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
|
@ -318,7 +318,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
const validPassword = validatePassword(password);
|
const validPassword = validatePassword(password);
|
||||||
|
|
||||||
const users = await Db.collections.User!.find({
|
const users = await Db.collections.User.find({
|
||||||
where: { id: In([inviterId, inviteeId]) },
|
where: { id: In([inviterId, inviteeId]) },
|
||||||
relations: ['globalRole'],
|
relations: ['globalRole'],
|
||||||
});
|
});
|
||||||
|
@ -352,7 +352,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
invitee.lastName = lastName;
|
invitee.lastName = lastName;
|
||||||
invitee.password = await hashPassword(validPassword);
|
invitee.password = await hashPassword(validPassword);
|
||||||
|
|
||||||
const updatedUser = await Db.collections.User!.save(invitee);
|
const updatedUser = await Db.collections.User.save(invitee);
|
||||||
|
|
||||||
await issueCookie(res, updatedUser);
|
await issueCookie(res, updatedUser);
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
this.app.get(
|
this.app.get(
|
||||||
`/${this.restEndpoint}/users`,
|
`/${this.restEndpoint}/users`,
|
||||||
ResponseHelper.send(async () => {
|
ResponseHelper.send(async () => {
|
||||||
const users = await Db.collections.User!.find({ relations: ['globalRole'] });
|
const users = await Db.collections.User.find({ relations: ['globalRole'] });
|
||||||
|
|
||||||
return users.map((user): PublicUser => sanitizeUser(user, ['personalizationAnswers']));
|
return users.map((user): PublicUser => sanitizeUser(user, ['personalizationAnswers']));
|
||||||
}),
|
}),
|
||||||
|
@ -400,7 +400,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await Db.collections.User!.find({
|
const users = await Db.collections.User.find({
|
||||||
where: { id: In([transferId, idToDelete]) },
|
where: { id: In([transferId, idToDelete]) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -434,11 +434,11 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
|
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
|
||||||
Db.collections.SharedWorkflow!.find({
|
Db.collections.SharedWorkflow.find({
|
||||||
relations: ['workflow'],
|
relations: ['workflow'],
|
||||||
where: { user: userToDelete },
|
where: { user: userToDelete },
|
||||||
}),
|
}),
|
||||||
Db.collections.SharedCredentials!.find({
|
Db.collections.SharedCredentials.find({
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: { user: userToDelete },
|
where: { user: userToDelete },
|
||||||
}),
|
}),
|
||||||
|
@ -496,7 +496,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reinvitee = await Db.collections.User!.findOne({ id: idToReinvite });
|
const reinvitee = await Db.collections.User.findOne({ id: idToReinvite });
|
||||||
|
|
||||||
if (!reinvitee) {
|
if (!reinvitee) {
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class WaitTrackerClass {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const executions = await Db.collections.Execution!.find(findQuery);
|
const executions = await Db.collections.Execution.find(findQuery);
|
||||||
|
|
||||||
if (executions.length === 0) {
|
if (executions.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -107,7 +107,7 @@ export class WaitTrackerClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check in database
|
// Also check in database
|
||||||
const execution = await Db.collections.Execution!.findOne(executionId);
|
const execution = await Db.collections.Execution.findOne(executionId);
|
||||||
|
|
||||||
if (execution === undefined || !execution.waitTill) {
|
if (execution === undefined || !execution.waitTill) {
|
||||||
throw new Error(`The execution ID "${executionId}" could not be found.`);
|
throw new Error(`The execution ID "${executionId}" could not be found.`);
|
||||||
|
@ -127,7 +127,7 @@ export class WaitTrackerClass {
|
||||||
fullExecutionData.stoppedAt = new Date();
|
fullExecutionData.stoppedAt = new Date();
|
||||||
fullExecutionData.waitTill = undefined;
|
fullExecutionData.waitTill = undefined;
|
||||||
|
|
||||||
await Db.collections.Execution!.update(
|
await Db.collections.Execution.update(
|
||||||
executionId,
|
executionId,
|
||||||
ResponseHelper.flattenExecutionData(fullExecutionData),
|
ResponseHelper.flattenExecutionData(fullExecutionData),
|
||||||
);
|
);
|
||||||
|
@ -146,7 +146,7 @@ export class WaitTrackerClass {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// Get the data to execute
|
// Get the data to execute
|
||||||
const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(executionId);
|
const fullExecutionDataFlatted = await Db.collections.Execution.findOne(executionId);
|
||||||
|
|
||||||
if (fullExecutionDataFlatted === undefined) {
|
if (fullExecutionDataFlatted === undefined) {
|
||||||
throw new Error(`The execution with the id "${executionId}" does not exist.`);
|
throw new Error(`The execution with the id "${executionId}" does not exist.`);
|
||||||
|
|
|
@ -300,7 +300,7 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (Db.collections.Workflow === null) {
|
if (!Db.isInitialized) {
|
||||||
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
return ResponseHelper.sendErrorResponse(res, error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,8 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!returnCredentials[type][nodeCredentials.id]) {
|
if (!returnCredentials[type][nodeCredentials.id]) {
|
||||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line no-await-in-loop
|
||||||
foundCredentials = await Db.collections.Credentials!.findOne({
|
foundCredentials = await Db.collections.Credentials.findOne({
|
||||||
id: nodeCredentials.id,
|
id: nodeCredentials.id,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|
|
@ -66,6 +66,7 @@ import {
|
||||||
getWorkflowOwner,
|
getWorkflowOwner,
|
||||||
} from './UserManagement/UserManagementHelper';
|
} from './UserManagement/UserManagementHelper';
|
||||||
import { whereClause } from './WorkflowHelpers';
|
import { whereClause } from './WorkflowHelpers';
|
||||||
|
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
|
@ -181,9 +182,7 @@ function pruneExecutionData(this: WorkflowHooks): void {
|
||||||
const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
|
const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
|
||||||
|
|
||||||
// throttle just on success to allow for self healing on failure
|
// throttle just on success to allow for self healing on failure
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
Db.collections.Execution.delete({ stoppedAt: LessThanOrEqual(utcDate) })
|
||||||
Db.collections
|
|
||||||
.Execution!.delete({ stoppedAt: LessThanOrEqual(utcDate) })
|
|
||||||
.then((data) =>
|
.then((data) =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
throttling = false;
|
throttling = false;
|
||||||
|
@ -371,8 +370,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
{ executionId: this.executionId, nodeName },
|
{ executionId: this.executionId, nodeName },
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const execution = await Db.collections.Execution.findOne(this.executionId);
|
||||||
const execution = await Db.collections.Execution!.findOne(this.executionId);
|
|
||||||
|
|
||||||
if (execution === undefined) {
|
if (execution === undefined) {
|
||||||
// Something went badly wrong if this happens.
|
// Something went badly wrong if this happens.
|
||||||
|
@ -418,8 +416,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
|
|
||||||
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
await Db.collections.Execution.update(
|
||||||
await Db.collections.Execution!.update(
|
|
||||||
this.executionId,
|
this.executionId,
|
||||||
flattenedExecutionData as IExecutionFlattedDb,
|
flattenedExecutionData as IExecutionFlattedDb,
|
||||||
);
|
);
|
||||||
|
@ -503,7 +500,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
|
|
||||||
if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) {
|
if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) {
|
||||||
// Data is always saved, so we remove from database
|
// Data is always saved, so we remove from database
|
||||||
await Db.collections.Execution!.delete(this.executionId);
|
await Db.collections.Execution.delete(this.executionId);
|
||||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
||||||
this.executionId,
|
this.executionId,
|
||||||
);
|
);
|
||||||
|
@ -539,7 +536,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Data is always saved, so we remove from database
|
// Data is always saved, so we remove from database
|
||||||
await Db.collections.Execution!.delete(this.executionId);
|
await Db.collections.Execution.delete(this.executionId);
|
||||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
||||||
this.executionId,
|
this.executionId,
|
||||||
);
|
);
|
||||||
|
@ -580,7 +577,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
// Save the Execution in DB
|
// Save the Execution in DB
|
||||||
await Db.collections.Execution!.update(
|
await Db.collections.Execution.update(
|
||||||
this.executionId,
|
this.executionId,
|
||||||
executionData as IExecutionFlattedDb,
|
executionData as IExecutionFlattedDb,
|
||||||
);
|
);
|
||||||
|
@ -588,7 +585,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
||||||
// If the retry was successful save the reference it on the original execution
|
// If the retry was successful save the reference it on the original execution
|
||||||
// await Db.collections.Execution!.save(executionData as IExecutionFlattedDb);
|
// await Db.collections.Execution!.save(executionData as IExecutionFlattedDb);
|
||||||
await Db.collections.Execution!.update(this.retryOf, {
|
await Db.collections.Execution.update(this.retryOf, {
|
||||||
retrySuccessId: this.executionId,
|
retrySuccessId: this.executionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -693,14 +690,14 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
// Save the Execution in DB
|
// Save the Execution in DB
|
||||||
await Db.collections.Execution!.update(
|
await Db.collections.Execution.update(
|
||||||
this.executionId,
|
this.executionId,
|
||||||
executionData as IExecutionFlattedDb,
|
executionData as IExecutionFlattedDb,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
||||||
// If the retry was successful save the reference it on the original execution
|
// If the retry was successful save the reference it on the original execution
|
||||||
await Db.collections.Execution!.update(this.retryOf, {
|
await Db.collections.Execution.update(this.retryOf, {
|
||||||
retrySuccessId: this.executionId,
|
retrySuccessId: this.executionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -792,7 +789,7 @@ export async function getWorkflowData(
|
||||||
|
|
||||||
let workflowData: IWorkflowBase | undefined;
|
let workflowData: IWorkflowBase | undefined;
|
||||||
if (workflowInfo.id !== undefined) {
|
if (workflowInfo.id !== undefined) {
|
||||||
if (Db.collections.Workflow === null) {
|
if (!Db.isInitialized) {
|
||||||
// The first time executeWorkflow gets called the Database has
|
// The first time executeWorkflow gets called the Database has
|
||||||
// to get initialized first
|
// to get initialized first
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
@ -804,7 +801,7 @@ export async function getWorkflowData(
|
||||||
relations = relations.filter((relation) => relation !== 'workflow.tags');
|
relations = relations.filter((relation) => relation !== 'workflow.tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
const shared = await Db.collections.SharedWorkflow!.findOne({
|
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||||
relations,
|
relations,
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user,
|
user,
|
||||||
|
@ -959,7 +956,7 @@ export async function executeWorkflow(
|
||||||
|
|
||||||
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
await Db.collections.Execution!.update(executionId, executionData as IExecutionFlattedDb);
|
await Db.collections.Execution.update(executionId, executionData as IExecutionFlattedDb);
|
||||||
throw {
|
throw {
|
||||||
...error,
|
...error,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
|
@ -1034,9 +1031,6 @@ export async function getBase(
|
||||||
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
|
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
|
||||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialsHelper: new CredentialsHelper(encryptionKey),
|
credentialsHelper: new CredentialsHelper(encryptionKey),
|
||||||
|
|
|
@ -107,9 +107,9 @@ export async function executeErrorWorkflow(
|
||||||
const user = await getWorkflowOwner(workflowErrorData.workflow.id!);
|
const user = await getWorkflowOwner(workflowErrorData.workflow.id!);
|
||||||
|
|
||||||
if (user.globalRole.name === 'owner') {
|
if (user.globalRole.name === 'owner') {
|
||||||
workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
|
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) });
|
||||||
} else {
|
} else {
|
||||||
const sharedWorkflowData = await Db.collections.SharedWorkflow!.findOne({
|
const sharedWorkflowData = await Db.collections.SharedWorkflow.findOne({
|
||||||
where: {
|
where: {
|
||||||
workflow: { id: workflowId },
|
workflow: { id: workflowId },
|
||||||
user,
|
user,
|
||||||
|
@ -121,7 +121,7 @@ export async function executeErrorWorkflow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
|
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
|
@ -426,7 +426,7 @@ export async function saveStaticDataById(
|
||||||
workflowId: string | number,
|
workflowId: string | number,
|
||||||
newStaticData: IDataObject,
|
newStaticData: IDataObject,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await Db.collections.Workflow!.update(workflowId, {
|
await Db.collections.Workflow.update(workflowId, {
|
||||||
staticData: newStaticData,
|
staticData: newStaticData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -440,7 +440,7 @@ export async function saveStaticDataById(
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export async function getStaticDataById(workflowId: string | number) {
|
export async function getStaticDataById(workflowId: string | number) {
|
||||||
const workflowData = await Db.collections.Workflow!.findOne(workflowId, {
|
const workflowData = await Db.collections.Workflow.findOne(workflowId, {
|
||||||
select: ['staticData'],
|
select: ['staticData'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -586,7 +586,7 @@ export function whereClause({
|
||||||
* Get the IDs of the workflows that have been shared with the user.
|
* Get the IDs of the workflows that have been shared with the user.
|
||||||
*/
|
*/
|
||||||
export async function getSharedWorkflowIds(user: User): Promise<number[]> {
|
export async function getSharedWorkflowIds(user: User): Promise<number[]> {
|
||||||
const sharedWorkflows = await Db.collections.SharedWorkflow!.find({
|
const sharedWorkflows = await Db.collections.SharedWorkflow.find({
|
||||||
relations: ['workflow'],
|
relations: ['workflow'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -513,7 +513,7 @@ export class WorkflowRunner {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionDb = (await Db.collections.Execution!.findOne(
|
const executionDb = (await Db.collections.Execution.findOne(
|
||||||
executionId,
|
executionId,
|
||||||
)) as IExecutionFlattedDb;
|
)) as IExecutionFlattedDb;
|
||||||
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
|
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
|
||||||
|
@ -548,7 +548,7 @@ export class WorkflowRunner {
|
||||||
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
||||||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
||||||
) {
|
) {
|
||||||
await Db.collections.Execution!.delete(executionId);
|
await Db.collections.Execution.delete(executionId);
|
||||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(executionId);
|
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(executionId);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line id-denylist
|
// eslint-disable-next-line id-denylist
|
||||||
|
|
|
@ -53,12 +53,12 @@ credentialsController.get(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (req.user.globalRole.name === 'owner') {
|
if (req.user.globalRole.name === 'owner') {
|
||||||
credentials = await Db.collections.Credentials!.find({
|
credentials = await Db.collections.Credentials.find({
|
||||||
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
|
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
|
||||||
where: filter,
|
where: filter,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shared = await Db.collections.SharedCredentials!.find({
|
const shared = await Db.collections.SharedCredentials.find({
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
entityType: 'credentials',
|
entityType: 'credentials',
|
||||||
|
@ -67,7 +67,7 @@ credentialsController.get(
|
||||||
|
|
||||||
if (!shared.length) return [];
|
if (!shared.length) return [];
|
||||||
|
|
||||||
credentials = await Db.collections.Credentials!.find({
|
credentials = await Db.collections.Credentials.find({
|
||||||
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
|
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
|
||||||
where: {
|
where: {
|
||||||
id: In(shared.map(({ credentialId }) => credentialId)),
|
id: In(shared.map(({ credentialId }) => credentialId)),
|
||||||
|
@ -115,13 +115,15 @@ credentialsController.post(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
||||||
const { credentials, nodeToTestWith } = req.body;
|
const { credentials, nodeToTestWith } = req.body;
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
return {
|
} catch (error) {
|
||||||
status: 'Error',
|
throw new ResponseHelper.ResponseError(
|
||||||
message: RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
};
|
undefined,
|
||||||
|
500,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const helper = new CredentialsHelper(encryptionKey);
|
const helper = new CredentialsHelper(encryptionKey);
|
||||||
|
@ -149,9 +151,10 @@ credentialsController.post(
|
||||||
nodeAccess.date = new Date();
|
nodeAccess.date = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
} catch (error) {
|
||||||
throw new ResponseHelper.ResponseError(
|
throw new ResponseHelper.ResponseError(
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -175,7 +178,7 @@ credentialsController.post(
|
||||||
|
|
||||||
await externalHooks.run('credentials.create', [encryptedData]);
|
await externalHooks.run('credentials.create', [encryptedData]);
|
||||||
|
|
||||||
const role = await Db.collections.Role!.findOneOrFail({
|
const role = await Db.collections.Role.findOneOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'credential',
|
scope: 'credential',
|
||||||
});
|
});
|
||||||
|
@ -213,7 +216,7 @@ credentialsController.delete(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
|
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
|
||||||
const { id: credentialId } = req.params;
|
const { id: credentialId } = req.params;
|
||||||
|
|
||||||
const shared = await Db.collections.SharedCredentials!.findOne({
|
const shared = await Db.collections.SharedCredentials.findOne({
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
@ -236,7 +239,7 @@ credentialsController.delete(
|
||||||
|
|
||||||
await externalHooks.run('credentials.delete', [credentialId]);
|
await externalHooks.run('credentials.delete', [credentialId]);
|
||||||
|
|
||||||
await Db.collections.Credentials!.remove(shared.credentials);
|
await Db.collections.Credentials.remove(shared.credentials);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
@ -255,7 +258,7 @@ credentialsController.patch(
|
||||||
|
|
||||||
await validateEntity(updateData);
|
await validateEntity(updateData);
|
||||||
|
|
||||||
const shared = await Db.collections.SharedCredentials!.findOne({
|
const shared = await Db.collections.SharedCredentials.findOne({
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
@ -285,9 +288,10 @@ credentialsController.patch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
} catch (error) {
|
||||||
throw new ResponseHelper.ResponseError(
|
throw new ResponseHelper.ResponseError(
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -329,11 +333,11 @@ credentialsController.patch(
|
||||||
await externalHooks.run('credentials.update', [newCredentialData]);
|
await externalHooks.run('credentials.update', [newCredentialData]);
|
||||||
|
|
||||||
// Update the credentials in DB
|
// Update the credentials in DB
|
||||||
await Db.collections.Credentials!.update(credentialId, newCredentialData);
|
await Db.collections.Credentials.update(credentialId, newCredentialData);
|
||||||
|
|
||||||
// We sadly get nothing back from "update". Neither if it updated a record
|
// We sadly get nothing back from "update". Neither if it updated a record
|
||||||
// nor the new value. So query now the updated entry.
|
// nor the new value. So query now the updated entry.
|
||||||
const responseData = await Db.collections.Credentials!.findOne(credentialId);
|
const responseData = await Db.collections.Credentials.findOne(credentialId);
|
||||||
|
|
||||||
if (responseData === undefined) {
|
if (responseData === undefined) {
|
||||||
throw new ResponseHelper.ResponseError(
|
throw new ResponseHelper.ResponseError(
|
||||||
|
@ -363,7 +367,7 @@ credentialsController.get(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Get) => {
|
ResponseHelper.send(async (req: CredentialRequest.Get) => {
|
||||||
const { id: credentialId } = req.params;
|
const { id: credentialId } = req.params;
|
||||||
|
|
||||||
const shared = await Db.collections.SharedCredentials!.findOne({
|
const shared = await Db.collections.SharedCredentials.findOne({
|
||||||
relations: ['credentials'],
|
relations: ['credentials'],
|
||||||
where: whereClause({
|
where: whereClause({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
@ -393,9 +397,10 @@ credentialsController.get(
|
||||||
|
|
||||||
const { data, id, ...rest } = credential;
|
const { data, id, ...rest } = credential;
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
let encryptionKey: string;
|
||||||
|
try {
|
||||||
if (!encryptionKey) {
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
} catch (error) {
|
||||||
throw new ResponseHelper.ResponseError(
|
throw new ResponseHelper.ResponseError(
|
||||||
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES } from 'n8n-core';
|
||||||
|
|
||||||
export const RESPONSE_ERROR_MESSAGES = {
|
export const RESPONSE_ERROR_MESSAGES = {
|
||||||
NO_CREDENTIAL: 'Credential not found',
|
NO_CREDENTIAL: 'Credential not found',
|
||||||
NO_ENCRYPTION_KEY: 'Encryption key missing to decrypt credentials',
|
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AUTH_COOKIE_NAME = 'n8n-auth';
|
export const AUTH_COOKIE_NAME = 'n8n-auth';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
BeforeInsert,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IsEmail, IsString, Length } from 'class-validator';
|
import { IsEmail, IsString, Length } from 'class-validator';
|
||||||
import * as config from '../../../config';
|
import * as config from '../../../config';
|
||||||
|
@ -20,7 +21,7 @@ import { Role } from './Role';
|
||||||
import { SharedWorkflow } from './SharedWorkflow';
|
import { SharedWorkflow } from './SharedWorkflow';
|
||||||
import { SharedCredentials } from './SharedCredentials';
|
import { SharedCredentials } from './SharedCredentials';
|
||||||
import { NoXss } from '../utils/customValidators';
|
import { NoXss } from '../utils/customValidators';
|
||||||
import { answersFormatter } from '../utils/transformers';
|
import { answersFormatter, lowerCaser } from '../utils/transformers';
|
||||||
|
|
||||||
export const MIN_PASSWORD_LENGTH = 8;
|
export const MIN_PASSWORD_LENGTH = 8;
|
||||||
|
|
||||||
|
@ -62,7 +63,11 @@ export class User {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ length: 254, nullable: true })
|
@Column({
|
||||||
|
length: 254,
|
||||||
|
nullable: true,
|
||||||
|
transformer: lowerCaser,
|
||||||
|
})
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -119,8 +124,10 @@ export class User {
|
||||||
})
|
})
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
@BeforeUpdate()
|
@BeforeUpdate()
|
||||||
setUpdateDate(): void {
|
preUpsertHook(): void {
|
||||||
|
this.email = this.email?.toLowerCase();
|
||||||
this.updatedAt = new Date();
|
this.updatedAt = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
|
||||||
|
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||||
|
name = 'LowerCaseUserEmail1648740597343';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE ${tablePrefix}user
|
||||||
|
SET email = LOWER(email);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
||||||
import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWorkflowCredentials';
|
import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWorkflowCredentials';
|
||||||
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
|
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -28,4 +29,5 @@ export const mysqlMigrations = [
|
||||||
UpdateWorkflowCredentials1630451444017,
|
UpdateWorkflowCredentials1630451444017,
|
||||||
AddExecutionEntityIndexes1644424784709,
|
AddExecutionEntityIndexes1644424784709,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
|
LowerCaseUserEmail1648740597343,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
|
||||||
|
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||||
|
name = 'LowerCaseUserEmail1648740597343';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let tablePrefix = config.get('database.tablePrefix');
|
||||||
|
const schema = config.get('database.postgresdb.schema');
|
||||||
|
if (schema) {
|
||||||
|
tablePrefix = schema + '.' + tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE ${tablePrefix}user
|
||||||
|
SET email = LOWER(email);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { UpdateWorkflowCredentials1630419189837 } from './1630419189837-UpdateWo
|
||||||
import { AddExecutionEntityIndexes1644422880309 } from './1644422880309-AddExecutionEntityIndexes';
|
import { AddExecutionEntityIndexes1644422880309 } from './1644422880309-AddExecutionEntityIndexes';
|
||||||
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
|
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -24,4 +25,5 @@ export const postgresMigrations = [
|
||||||
AddExecutionEntityIndexes1644422880309,
|
AddExecutionEntityIndexes1644422880309,
|
||||||
IncreaseTypeVarcharLimit1646834195327,
|
IncreaseTypeVarcharLimit1646834195327,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
|
LowerCaseUserEmail1648740597343,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||||
|
name = 'LowerCaseUserEmail1648740597343';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE "${tablePrefix}user"
|
||||||
|
SET email = LOWER(email);
|
||||||
|
`);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
||||||
import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials';
|
import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials';
|
||||||
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
|
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
import { AddAPIKeyColumn1647888658687 } from './1647888658687-AddAPIKeyColumn';
|
import { AddAPIKeyColumn1647888658687 } from './1647888658687-AddAPIKeyColumn';
|
||||||
|
|
||||||
const sqliteMigrations = [
|
const sqliteMigrations = [
|
||||||
|
@ -25,6 +26,7 @@ const sqliteMigrations = [
|
||||||
UpdateWorkflowCredentials1630330987096,
|
UpdateWorkflowCredentials1630330987096,
|
||||||
AddExecutionEntityIndexes1644421939510,
|
AddExecutionEntityIndexes1644421939510,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
|
LowerCaseUserEmail1648740597343,
|
||||||
AddAPIKeyColumn1647888658687,
|
AddAPIKeyColumn1647888658687,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
import { IPersonalizationSurveyAnswers } from '../../Interfaces';
|
import { IPersonalizationSurveyAnswers } from '../../Interfaces';
|
||||||
|
|
||||||
export const idStringifier = {
|
export const idStringifier = {
|
||||||
from: (value: number): string | number => (value ? value.toString() : value),
|
from: (value: number): string | number => (typeof value === 'number' ? value.toString() : value),
|
||||||
to: (value: string): number | string => (value ? Number(value) : value),
|
to: (value: string): number | string => (typeof value === 'string' ? Number(value) : value),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lowerCaser = {
|
||||||
|
from: (value: string): string => value,
|
||||||
|
to: (value: string): string => (typeof value === 'string' ? value.toLowerCase() : value),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
8
packages/cli/src/requests.d.ts
vendored
8
packages/cli/src/requests.d.ts
vendored
|
@ -255,12 +255,16 @@ export declare namespace OAuthRequest {
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{ oauth_verifier: string; oauth_token: string; cid: string }
|
{ oauth_verifier: string; oauth_token: string; cid: string }
|
||||||
>;
|
> & {
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace OAuth2Credential {
|
namespace OAuth2Credential {
|
||||||
type Auth = OAuth1Credential.Auth;
|
type Auth = OAuth1Credential.Auth;
|
||||||
type Callback = AuthenticatedRequest<{}, {}, {}, { code: string; state: string }>;
|
type Callback = AuthenticatedRequest<{}, {}, {}, { code: string; state: string }> & {
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { hashSync, genSaltSync } from 'bcryptjs';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
@ -60,10 +59,18 @@ afterAll(async () => {
|
||||||
test('POST /login should log user in', async () => {
|
test('POST /login should log user in', async () => {
|
||||||
const authlessAgent = utils.createAgent(app);
|
const authlessAgent = utils.createAgent(app);
|
||||||
|
|
||||||
const response = await authlessAgent.post('/login').send({
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{
|
||||||
email: TEST_USER.email,
|
email: TEST_USER.email,
|
||||||
password: TEST_USER.password,
|
password: TEST_USER.password,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
email: TEST_USER.email.toUpperCase(),
|
||||||
|
password: TEST_USER.password,
|
||||||
|
},
|
||||||
|
].map(async (payload) => {
|
||||||
|
const response = await authlessAgent.post('/login').send(payload);
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
@ -84,7 +91,6 @@ test('POST /login should log user in', async () => {
|
||||||
expect(lastName).toBe(TEST_USER.lastName);
|
expect(lastName).toBe(TEST_USER.lastName);
|
||||||
expect(password).toBeUndefined();
|
expect(password).toBeUndefined();
|
||||||
expect(personalizationAnswers).toBeNull();
|
expect(personalizationAnswers).toBeNull();
|
||||||
expect(password).toBeUndefined();
|
|
||||||
expect(resetPasswordToken).toBeUndefined();
|
expect(resetPasswordToken).toBeUndefined();
|
||||||
expect(globalRole).toBeDefined();
|
expect(globalRole).toBeDefined();
|
||||||
expect(globalRole.name).toBe('owner');
|
expect(globalRole.name).toBe('owner');
|
||||||
|
@ -92,6 +98,8 @@ test('POST /login should log user in', async () => {
|
||||||
|
|
||||||
const authToken = utils.getAuthToken(response);
|
const authToken = utils.getAuthToken(response);
|
||||||
expect(authToken).toBeDefined();
|
expect(authToken).toBeDefined();
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /login should receive logged in user', async () => {
|
test('GET /login should receive logged in user', async () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { CredentialPayload, SaveCredentialFunction } from './shared/types';
|
||||||
import type { Role } from '../../src/databases/entities/Role';
|
import type { Role } from '../../src/databases/entities/Role';
|
||||||
import type { User } from '../../src/databases/entities/User';
|
import type { User } from '../../src/databases/entities/User';
|
||||||
import * as testDb from './shared/testDb';
|
import * as testDb from './shared/testDb';
|
||||||
|
import { RESPONSE_ERROR_MESSAGES } from '../../src/constants';
|
||||||
import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity';
|
import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity';
|
||||||
|
|
||||||
jest.mock('../../src/telemetry');
|
jest.mock('../../src/telemetry');
|
||||||
|
@ -91,7 +92,7 @@ test('POST /credentials should fail with invalid inputs', async () => {
|
||||||
|
|
||||||
test('POST /credentials should fail with missing encryption key', async () => {
|
test('POST /credentials should fail with missing encryption key', async () => {
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
||||||
mock.mockResolvedValue(undefined);
|
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
||||||
|
|
||||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
@ -354,7 +355,8 @@ test('PATCH /credentials/:id should fail if cred not found', async () => {
|
||||||
|
|
||||||
test('PATCH /credentials/:id should fail with missing encryption key', async () => {
|
test('PATCH /credentials/:id should fail with missing encryption key', async () => {
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
||||||
mock.mockResolvedValue(undefined);
|
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
||||||
|
|
||||||
|
|
||||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
@ -504,7 +506,8 @@ test('GET /credentials/:id should fail with missing encryption key', async () =>
|
||||||
const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
|
const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
|
||||||
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
||||||
mock.mockResolvedValue(undefined);
|
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
||||||
|
|
||||||
|
|
||||||
const response = await authOwnerAgent
|
const response = await authOwnerAgent
|
||||||
.get(`/credentials/${savedCredential.id}`)
|
.get(`/credentials/${savedCredential.id}`)
|
||||||
|
|
|
@ -91,7 +91,7 @@ describe('Owner shell', () => {
|
||||||
} = response.body.data;
|
} = response.body.data;
|
||||||
|
|
||||||
expect(validator.isUUID(id)).toBe(true);
|
expect(validator.isUUID(id)).toBe(true);
|
||||||
expect(email).toBe(validPayload.email);
|
expect(email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(firstName).toBe(validPayload.firstName);
|
expect(firstName).toBe(validPayload.firstName);
|
||||||
expect(lastName).toBe(validPayload.lastName);
|
expect(lastName).toBe(validPayload.lastName);
|
||||||
expect(personalizationAnswers).toBeNull();
|
expect(personalizationAnswers).toBeNull();
|
||||||
|
@ -103,7 +103,7 @@ describe('Owner shell', () => {
|
||||||
|
|
||||||
const storedOwnerShell = await Db.collections.User!.findOneOrFail(id);
|
const storedOwnerShell = await Db.collections.User!.findOneOrFail(id);
|
||||||
|
|
||||||
expect(storedOwnerShell.email).toBe(validPayload.email);
|
expect(storedOwnerShell.email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(storedOwnerShell.firstName).toBe(validPayload.firstName);
|
expect(storedOwnerShell.firstName).toBe(validPayload.firstName);
|
||||||
expect(storedOwnerShell.lastName).toBe(validPayload.lastName);
|
expect(storedOwnerShell.lastName).toBe(validPayload.lastName);
|
||||||
}
|
}
|
||||||
|
@ -245,7 +245,7 @@ describe('Member', () => {
|
||||||
} = response.body.data;
|
} = response.body.data;
|
||||||
|
|
||||||
expect(validator.isUUID(id)).toBe(true);
|
expect(validator.isUUID(id)).toBe(true);
|
||||||
expect(email).toBe(validPayload.email);
|
expect(email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(firstName).toBe(validPayload.firstName);
|
expect(firstName).toBe(validPayload.firstName);
|
||||||
expect(lastName).toBe(validPayload.lastName);
|
expect(lastName).toBe(validPayload.lastName);
|
||||||
expect(personalizationAnswers).toBeNull();
|
expect(personalizationAnswers).toBeNull();
|
||||||
|
@ -257,7 +257,7 @@ describe('Member', () => {
|
||||||
|
|
||||||
const storedMember = await Db.collections.User!.findOneOrFail(id);
|
const storedMember = await Db.collections.User!.findOneOrFail(id);
|
||||||
|
|
||||||
expect(storedMember.email).toBe(validPayload.email);
|
expect(storedMember.email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(storedMember.firstName).toBe(validPayload.firstName);
|
expect(storedMember.firstName).toBe(validPayload.firstName);
|
||||||
expect(storedMember.lastName).toBe(validPayload.lastName);
|
expect(storedMember.lastName).toBe(validPayload.lastName);
|
||||||
}
|
}
|
||||||
|
@ -400,7 +400,7 @@ describe('Owner', () => {
|
||||||
} = response.body.data;
|
} = response.body.data;
|
||||||
|
|
||||||
expect(validator.isUUID(id)).toBe(true);
|
expect(validator.isUUID(id)).toBe(true);
|
||||||
expect(email).toBe(validPayload.email);
|
expect(email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(firstName).toBe(validPayload.firstName);
|
expect(firstName).toBe(validPayload.firstName);
|
||||||
expect(lastName).toBe(validPayload.lastName);
|
expect(lastName).toBe(validPayload.lastName);
|
||||||
expect(personalizationAnswers).toBeNull();
|
expect(personalizationAnswers).toBeNull();
|
||||||
|
@ -412,19 +412,13 @@ describe('Owner', () => {
|
||||||
|
|
||||||
const storedOwner = await Db.collections.User!.findOneOrFail(id);
|
const storedOwner = await Db.collections.User!.findOneOrFail(id);
|
||||||
|
|
||||||
expect(storedOwner.email).toBe(validPayload.email);
|
expect(storedOwner.email).toBe(validPayload.email.toLowerCase());
|
||||||
expect(storedOwner.firstName).toBe(validPayload.firstName);
|
expect(storedOwner.firstName).toBe(validPayload.firstName);
|
||||||
expect(storedOwner.lastName).toBe(validPayload.lastName);
|
expect(storedOwner.lastName).toBe(validPayload.lastName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const TEST_USER = {
|
|
||||||
email: randomEmail(),
|
|
||||||
firstName: randomName(),
|
|
||||||
lastName: randomName(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const SURVEY = [
|
const SURVEY = [
|
||||||
'codingSkill',
|
'codingSkill',
|
||||||
'companyIndustry',
|
'companyIndustry',
|
||||||
|
@ -444,7 +438,7 @@ const VALID_PATCH_ME_PAYLOADS = [
|
||||||
password: randomValidPassword(),
|
password: randomValidPassword(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: randomEmail(),
|
email: randomEmail().toUpperCase(),
|
||||||
firstName: randomName(),
|
firstName: randomName(),
|
||||||
lastName: randomName(),
|
lastName: randomName(),
|
||||||
password: randomValidPassword(),
|
password: randomValidPassword(),
|
||||||
|
|
|
@ -30,6 +30,12 @@ beforeAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
jest.mock('../../config');
|
||||||
|
|
||||||
|
config.set('userManagement.isInstanceOwnerSetUp', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
await testDb.truncate(['User'], testDbName);
|
await testDb.truncate(['User'], testDbName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,6 +94,29 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
|
||||||
expect(isInstanceOwnerSetUpSetting).toBe(true);
|
expect(isInstanceOwnerSetUpSetting).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('POST /owner should create owner with lowercased email', async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const newOwnerData = {
|
||||||
|
email: randomEmail().toUpperCase(),
|
||||||
|
firstName: randomName(),
|
||||||
|
lastName: randomName(),
|
||||||
|
password: randomValidPassword(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.post('/owner').send(newOwnerData);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const { id, email } = response.body.data;
|
||||||
|
|
||||||
|
expect(email).toBe(newOwnerData.email.toLowerCase());
|
||||||
|
|
||||||
|
const storedOwner = await Db.collections.User!.findOneOrFail(id);
|
||||||
|
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
test('POST /owner should fail with invalid inputs', async () => {
|
test('POST /owner should fail with invalid inputs', async () => {
|
||||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
|
@ -13,12 +13,14 @@ import {
|
||||||
} from './shared/random';
|
} from './shared/random';
|
||||||
import * as testDb from './shared/testDb';
|
import * as testDb from './shared/testDb';
|
||||||
import type { Role } from '../../src/databases/entities/Role';
|
import type { Role } from '../../src/databases/entities/Role';
|
||||||
|
import { SMTP_TEST_TIMEOUT } from './shared/constants';
|
||||||
|
|
||||||
jest.mock('../../src/telemetry');
|
jest.mock('../../src/telemetry');
|
||||||
|
|
||||||
let app: express.Application;
|
let app: express.Application;
|
||||||
let testDbName = '';
|
let testDbName = '';
|
||||||
let globalOwnerRole: Role;
|
let globalOwnerRole: Role;
|
||||||
|
let globalMemberRole: Role;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true });
|
app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true });
|
||||||
|
@ -26,6 +28,7 @@ beforeAll(async () => {
|
||||||
testDbName = initResult.testDbName;
|
testDbName = initResult.testDbName;
|
||||||
|
|
||||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||||
|
globalMemberRole = await testDb.getGlobalMemberRole();
|
||||||
|
|
||||||
utils.initTestTelemetry();
|
utils.initTestTelemetry();
|
||||||
utils.initTestLogger();
|
utils.initTestLogger();
|
||||||
|
@ -38,30 +41,40 @@ beforeEach(async () => {
|
||||||
|
|
||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
config.set('userManagement.emails.mode', '');
|
config.set('userManagement.emails.mode', '');
|
||||||
|
|
||||||
jest.setTimeout(30000); // fake SMTP service might be slow
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await testDb.terminate(testDbName);
|
await testDb.terminate(testDbName);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /forgot-password should send password reset email', async () => {
|
test(
|
||||||
|
'POST /forgot-password should send password reset email',
|
||||||
|
async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
|
||||||
const authlessAgent = utils.createAgent(app);
|
const authlessAgent = utils.createAgent(app);
|
||||||
|
const member = await testDb.createUser({
|
||||||
|
email: 'test@test.com',
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
});
|
||||||
|
|
||||||
await utils.configureSmtp();
|
await utils.configureSmtp();
|
||||||
|
|
||||||
const response = await authlessAgent.post('/forgot-password').send({ email: owner.email });
|
await Promise.all(
|
||||||
|
[{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => {
|
||||||
|
const response = await authlessAgent.post('/forgot-password').send(payload);
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
expect(response.body).toEqual({});
|
expect(response.body).toEqual({});
|
||||||
|
|
||||||
const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
|
const user = await Db.collections.User!.findOneOrFail({ email: payload.email });
|
||||||
expect(storedOwner.resetPasswordToken).toBeDefined();
|
expect(user.resetPasswordToken).toBeDefined();
|
||||||
expect(storedOwner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
|
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
SMTP_TEST_TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
test('POST /forgot-password should fail if emailing is not set up', async () => {
|
test('POST /forgot-password should fail if emailing is not set up', async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
|
|
@ -59,3 +59,8 @@ export const BOOTSTRAP_POSTGRES_CONNECTION_NAME: Readonly<string> = 'n8n_bs_post
|
||||||
* for each suite test run.
|
* for each suite test run.
|
||||||
*/
|
*/
|
||||||
export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
|
export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
|
||||||
|
*/
|
||||||
|
export const SMTP_TEST_TIMEOUT = 30_000;
|
||||||
|
|
|
@ -395,10 +395,6 @@ export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions =
|
||||||
async function encryptCredentialData(credential: CredentialsEntity) {
|
async function encryptCredentialData(credential: CredentialsEntity) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
if (!encryptionKey) {
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
const coreCredential = new Credentials(
|
const coreCredential = new Credentials(
|
||||||
{ id: null, name: credential.name },
|
{ id: null, name: credential.name },
|
||||||
credential.type,
|
credential.type,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { Db } from '../../src';
|
import { Db } from '../../src';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
|
import { SMTP_TEST_TIMEOUT, SUCCESS_RESPONSE_BODY } from './shared/constants';
|
||||||
import {
|
import {
|
||||||
randomEmail,
|
randomEmail,
|
||||||
randomValidPassword,
|
randomValidPassword,
|
||||||
|
@ -47,8 +47,6 @@ beforeAll(async () => {
|
||||||
|
|
||||||
utils.initTestTelemetry();
|
utils.initTestTelemetry();
|
||||||
utils.initTestLogger();
|
utils.initTestLogger();
|
||||||
|
|
||||||
jest.setTimeout(30000); // fake SMTP service might be slow
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -481,13 +479,22 @@ test('POST /users should fail if user management is disabled', async () => {
|
||||||
expect(response.statusCode).toBe(500);
|
expect(response.statusCode).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /users should email invites and create user shells', async () => {
|
test(
|
||||||
|
'POST /users should email invites and create user shells but ignore existing',
|
||||||
|
async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
const memberShell = await testDb.createUserShell(globalMemberRole);
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||||
|
|
||||||
await utils.configureSmtp();
|
await utils.configureSmtp();
|
||||||
|
|
||||||
const testEmails = [randomEmail(), randomEmail(), randomEmail()];
|
const testEmails = [
|
||||||
|
randomEmail(),
|
||||||
|
randomEmail().toUpperCase(),
|
||||||
|
memberShell.email,
|
||||||
|
member.email,
|
||||||
|
];
|
||||||
|
|
||||||
const payload = testEmails.map((e) => ({ email: e }));
|
const payload = testEmails.map((e) => ({ email: e }));
|
||||||
|
|
||||||
|
@ -495,12 +502,17 @@ test('POST /users should email invites and create user shells', async () => {
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
await Promise.all(
|
for (const {
|
||||||
response.body.data.map(async ({ user, error }: { user: User; error: Error }) => {
|
user: { id, email: receivedEmail },
|
||||||
const { id, email: receivedEmail } = user;
|
error,
|
||||||
|
} of response.body.data) {
|
||||||
expect(validator.isUUID(id)).toBe(true);
|
expect(validator.isUUID(id)).toBe(true);
|
||||||
expect(testEmails.some((e) => e === receivedEmail)).toBe(true);
|
expect(id).not.toBe(member.id);
|
||||||
|
|
||||||
|
const lowerCasedEmail = receivedEmail.toLowerCase();
|
||||||
|
expect(receivedEmail).toBe(lowerCasedEmail);
|
||||||
|
expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
expect(error).toBe('Email could not be sent');
|
expect(error).toBe('Email could not be sent');
|
||||||
}
|
}
|
||||||
|
@ -514,11 +526,14 @@ test('POST /users should email invites and create user shells', async () => {
|
||||||
expect(personalizationAnswers).toBeNull();
|
expect(personalizationAnswers).toBeNull();
|
||||||
expect(password).toBeNull();
|
expect(password).toBeNull();
|
||||||
expect(resetPasswordToken).toBeNull();
|
expect(resetPasswordToken).toBeNull();
|
||||||
}),
|
}
|
||||||
|
},
|
||||||
|
SMTP_TEST_TIMEOUT,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
test('POST /users should fail with invalid inputs', async () => {
|
test(
|
||||||
|
'POST /users should fail with invalid inputs',
|
||||||
|
async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||||
|
|
||||||
|
@ -541,9 +556,13 @@ test('POST /users should fail with invalid inputs', async () => {
|
||||||
expect(users.length).toBe(1); // DB unaffected
|
expect(users.length).toBe(1); // DB unaffected
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
SMTP_TEST_TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
test('POST /users should ignore an empty payload', async () => {
|
test(
|
||||||
|
'POST /users should ignore an empty payload',
|
||||||
|
async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||||
|
|
||||||
|
@ -559,7 +578,9 @@ test('POST /users should ignore an empty payload', async () => {
|
||||||
|
|
||||||
const users = await Db.collections.User!.find();
|
const users = await Db.collections.User!.find();
|
||||||
expect(users.length).toBe(1);
|
expect(users.length).toBe(1);
|
||||||
});
|
},
|
||||||
|
SMTP_TEST_TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: /users/:id/reinvite route tests missing
|
// TODO: /users/:id/reinvite route tests missing
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.112.0",
|
"version": "0.115.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n8n-workflow": "~0.94.0",
|
"n8n-workflow": "~0.97.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
export const BINARY_ENCODING = 'base64';
|
export const BINARY_ENCODING = 'base64';
|
||||||
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
||||||
export const ENCRYPTION_KEY_ENV_OVERWRITE = 'N8N_ENCRYPTION_KEY';
|
export const ENCRYPTION_KEY_ENV_OVERWRITE = 'N8N_ENCRYPTION_KEY';
|
||||||
|
@ -9,3 +10,7 @@ export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKOWN__';
|
||||||
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
||||||
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
|
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
|
||||||
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
||||||
|
|
||||||
|
export const RESPONSE_ERROR_MESSAGES = {
|
||||||
|
NO_ENCRYPTION_KEY: 'Encryption key is missing or was not set',
|
||||||
|
};
|
||||||
|
|
|
@ -874,13 +874,7 @@ export async function requestOAuth2(
|
||||||
oAuth2Options?: IOAuth2Options,
|
oAuth2Options?: IOAuth2Options,
|
||||||
isN8nRequest = false,
|
isN8nRequest = false,
|
||||||
) {
|
) {
|
||||||
const credentials = (await this.getCredentials(
|
const credentials = await this.getCredentials(credentialsType);
|
||||||
credentialsType,
|
|
||||||
)) as ICredentialDataDecryptedObject;
|
|
||||||
|
|
||||||
if (credentials === undefined) {
|
|
||||||
throw new Error('No credentials were returned!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credentials.oauthTokenData === undefined) {
|
if (credentials.oauthTokenData === undefined) {
|
||||||
throw new Error('OAuth credentials not connected!');
|
throw new Error('OAuth credentials not connected!');
|
||||||
|
@ -997,9 +991,7 @@ export async function requestOAuth1(
|
||||||
| IHttpRequestOptions,
|
| IHttpRequestOptions,
|
||||||
isN8nRequest = false,
|
isN8nRequest = false,
|
||||||
) {
|
) {
|
||||||
const credentials = (await this.getCredentials(
|
const credentials = await this.getCredentials(credentialsType);
|
||||||
credentialsType,
|
|
||||||
)) as ICredentialDataDecryptedObject;
|
|
||||||
|
|
||||||
if (credentials === undefined) {
|
if (credentials === undefined) {
|
||||||
throw new Error('No credentials were returned!');
|
throw new Error('No credentials were returned!');
|
||||||
|
@ -1269,7 +1261,7 @@ export async function getCredentials(
|
||||||
runIndex?: number,
|
runIndex?: number,
|
||||||
connectionInputData?: INodeExecutionData[],
|
connectionInputData?: INodeExecutionData[],
|
||||||
itemIndex?: number,
|
itemIndex?: number,
|
||||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
// Get the NodeType as it has the information if the credentials are required
|
// Get the NodeType as it has the information if the credentials are required
|
||||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||||
if (nodeType === undefined) {
|
if (nodeType === undefined) {
|
||||||
|
@ -1309,8 +1301,8 @@ export async function getCredentials(
|
||||||
node.parameters,
|
node.parameters,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Credentials should not be displayed so return undefined even if they would be defined
|
// Credentials should not be displayed even if they would be defined
|
||||||
return undefined;
|
throw new NodeOperationError(node, 'Credentials not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1327,15 +1319,15 @@ export async function getCredentials(
|
||||||
throw new NodeOperationError(node, `Node does not have any credentials set for "${type}"!`);
|
throw new NodeOperationError(node, `Node does not have any credentials set for "${type}"!`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Credentials are not required so resolve with undefined
|
// Credentials are not required
|
||||||
return undefined;
|
throw new NodeOperationError(node, 'Node does not require credentials');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullAccess && (!node.credentials || !node.credentials[type])) {
|
if (fullAccess && (!node.credentials || !node.credentials[type])) {
|
||||||
// Make sure that fullAccess nodes still behave like before that if they
|
// Make sure that fullAccess nodes still behave like before that if they
|
||||||
// request access to credentials that are currently not set it returns undefined
|
// request access to credentials that are currently not set it returns undefined
|
||||||
return undefined;
|
throw new NodeOperationError(node, 'Credentials not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
|
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
|
||||||
|
@ -1605,7 +1597,7 @@ export function getExecutePollFunctions(
|
||||||
__emit: (data: INodeExecutionData[][]): void => {
|
__emit: (data: INodeExecutionData[][]): void => {
|
||||||
throw new Error('Overwrite NodeExecuteFunctions.getExecutePullFunctions.__emit function!');
|
throw new Error('Overwrite NodeExecuteFunctions.getExecutePullFunctions.__emit function!');
|
||||||
},
|
},
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(workflow, node, type, additionalData, mode);
|
return getCredentials(workflow, node, type, additionalData, mode);
|
||||||
},
|
},
|
||||||
getMode: (): WorkflowExecuteMode => {
|
getMode: (): WorkflowExecuteMode => {
|
||||||
|
@ -1759,7 +1751,7 @@ export function getExecuteTriggerFunctions(
|
||||||
emitError: (error: Error): void => {
|
emitError: (error: Error): void => {
|
||||||
throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!');
|
throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!');
|
||||||
},
|
},
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(workflow, node, type, additionalData, mode);
|
return getCredentials(workflow, node, type, additionalData, mode);
|
||||||
},
|
},
|
||||||
getNode: () => {
|
getNode: () => {
|
||||||
|
@ -1949,7 +1941,7 @@ export function getExecuteFunctions(
|
||||||
async getCredentials(
|
async getCredentials(
|
||||||
type: string,
|
type: string,
|
||||||
itemIndex?: number,
|
itemIndex?: number,
|
||||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(
|
return getCredentials(
|
||||||
workflow,
|
workflow,
|
||||||
node,
|
node,
|
||||||
|
@ -2193,7 +2185,7 @@ export function getExecuteSingleFunctions(
|
||||||
getContext(type: string): IContextObject {
|
getContext(type: string): IContextObject {
|
||||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||||
},
|
},
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(
|
return getCredentials(
|
||||||
workflow,
|
workflow,
|
||||||
node,
|
node,
|
||||||
|
@ -2389,7 +2381,7 @@ export function getLoadOptionsFunctions(
|
||||||
): ILoadOptionsFunctions {
|
): ILoadOptionsFunctions {
|
||||||
return ((workflow: Workflow, node: INode, path: string) => {
|
return ((workflow: Workflow, node: INode, path: string) => {
|
||||||
const that = {
|
const that = {
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(workflow, node, type, additionalData, 'internal');
|
return getCredentials(workflow, node, type, additionalData, 'internal');
|
||||||
},
|
},
|
||||||
getCurrentNodeParameter: (
|
getCurrentNodeParameter: (
|
||||||
|
@ -2533,7 +2525,7 @@ export function getExecuteHookFunctions(
|
||||||
): IHookFunctions {
|
): IHookFunctions {
|
||||||
return ((workflow: Workflow, node: INode) => {
|
return ((workflow: Workflow, node: INode) => {
|
||||||
const that = {
|
const that = {
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(workflow, node, type, additionalData, mode);
|
return getCredentials(workflow, node, type, additionalData, mode);
|
||||||
},
|
},
|
||||||
getMode: (): WorkflowExecuteMode => {
|
getMode: (): WorkflowExecuteMode => {
|
||||||
|
@ -2692,7 +2684,7 @@ export function getExecuteWebhookFunctions(
|
||||||
}
|
}
|
||||||
return additionalData.httpRequest.body;
|
return additionalData.httpRequest.body;
|
||||||
},
|
},
|
||||||
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
|
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return getCredentials(workflow, node, type, additionalData, mode);
|
return getCredentials(workflow, node, type, additionalData, mode);
|
||||||
},
|
},
|
||||||
getHeaderData(): object {
|
getHeaderData(): object {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ENCRYPTION_KEY_ENV_OVERWRITE,
|
ENCRYPTION_KEY_ENV_OVERWRITE,
|
||||||
EXTENSIONS_SUBDIRECTORY,
|
EXTENSIONS_SUBDIRECTORY,
|
||||||
IUserSettings,
|
IUserSettings,
|
||||||
|
RESPONSE_ERROR_MESSAGES,
|
||||||
USER_FOLDER_ENV_OVERWRITE,
|
USER_FOLDER_ENV_OVERWRITE,
|
||||||
USER_SETTINGS_FILE_NAME,
|
USER_SETTINGS_FILE_NAME,
|
||||||
USER_SETTINGS_SUBFOLDER,
|
USER_SETTINGS_SUBFOLDER,
|
||||||
|
@ -73,19 +74,15 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function getEncryptionKey(): Promise<string | undefined> {
|
export async function getEncryptionKey(): Promise<string> {
|
||||||
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
||||||
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSettings = await getUserSettings();
|
const userSettings = await getUserSettings();
|
||||||
|
|
||||||
if (userSettings === undefined) {
|
if (userSettings === undefined || userSettings.encryptionKey === undefined) {
|
||||||
return undefined;
|
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.encryptionKey === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return userSettings.encryptionKey;
|
return userSettings.encryptionKey;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-design-system",
|
"name": "n8n-design-system",
|
||||||
"version": "0.16.0",
|
"version": "0.18.0",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -79,6 +79,7 @@
|
||||||
"vue-loader": "^15.9.7",
|
"vue-loader": "^15.9.7",
|
||||||
"vue-property-decorator": "^9.1.2",
|
"vue-property-decorator": "^9.1.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vue-typed-mixins": "^0.2.0",
|
||||||
"vue2-boring-avatars": "0.3.4",
|
"vue2-boring-avatars": "0.3.4",
|
||||||
"xss": "^1.0.10"
|
"xss": "^1.0.10"
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,6 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
},
|
},
|
||||||
round: {
|
round: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<component
|
<component
|
||||||
:is="$options.components.N8nText"
|
:is="$options.components.N8nText"
|
||||||
:size="props.size"
|
:size="props.size"
|
||||||
|
:color="props.color"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
|
@ -25,7 +26,6 @@ export default {
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
@ -36,6 +36,8 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
color: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -40,7 +40,6 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
|
|
@ -11,7 +11,13 @@ const Template = (args, { argTypes }) => ({
|
||||||
N8nInfoTip,
|
N8nInfoTip,
|
||||||
},
|
},
|
||||||
template:
|
template:
|
||||||
'<n8n-info-tip>Need help doing something? <a href="/docs" target="_blank">Open docs</a></n8n-info-tip>',
|
'<n8n-info-tip v-bind="$props">Need help doing something? <a href="/docs" target="_blank">Open docs</a></n8n-info-tip>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const InputLabel = Template.bind({});
|
export const Note = Template.bind({});
|
||||||
|
|
||||||
|
export const Tooltip = Template.bind({});
|
||||||
|
Tooltip.args = {
|
||||||
|
type: 'tooltip',
|
||||||
|
tooltipPlacement: 'right',
|
||||||
|
};
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
<template functional>
|
<template>
|
||||||
<div :class="$style.infotip">
|
<div :class="[$style[theme], $style[type]]">
|
||||||
<component :is="$options.components.N8nIcon" icon="info-circle" /> <span><slot></slot></span>
|
<n8n-tooltip :placement="tooltipPlacement" :popper-class="$style.tooltipPopper" :disabled="type !== 'tooltip'">
|
||||||
|
<span>
|
||||||
|
<n8n-icon :icon="theme.startsWith('info') ? 'info-circle': 'exclamation-triangle'" />
|
||||||
|
<span v-if="type === 'note'"><slot></slot></span>
|
||||||
|
</span>
|
||||||
|
<span v-if="type === 'tooltip'" slot="content"><slot></slot></span>
|
||||||
|
</n8n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
|
import N8nTooltip from '../N8nTooltip';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'n8n-info-tip',
|
name: 'n8n-info-tip',
|
||||||
components: {
|
components: {
|
||||||
N8nIcon,
|
N8nIcon,
|
||||||
|
N8nTooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'info',
|
||||||
|
validator: (value: string): boolean =>
|
||||||
|
['info', 'info-light', 'warning', 'danger'].includes(value),
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'note',
|
||||||
|
validator: (value: string): boolean =>
|
||||||
|
['note', 'tooltip'].includes(value),
|
||||||
|
},
|
||||||
|
tooltipPlacement: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.infotip {
|
.base {
|
||||||
color: var(--color-text-light);
|
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
line-height: var(--font-size-s);
|
line-height: var(--font-size-s);
|
||||||
|
@ -27,7 +52,35 @@ export default {
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
composes: base;
|
||||||
|
|
||||||
|
svg {
|
||||||
margin-right: var(--spacing-4xs);
|
margin-right: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
composes: base;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-light {
|
||||||
|
color: var(--color-foreground-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!loading" ref="editor" :class="$style.markdown" v-html="htmlContent" />
|
<div
|
||||||
|
v-if="!loading"
|
||||||
|
ref="editor"
|
||||||
|
:class="$style[theme]" v-html="htmlContent"
|
||||||
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
<div v-for="(block, index) in loadingBlocks"
|
<div v-for="(block, index) in loadingBlocks"
|
||||||
:key="index">
|
:key="index">
|
||||||
|
@ -59,6 +63,9 @@ export default {
|
||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
withMultiBreaks: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
@ -75,6 +82,10 @@ export default {
|
||||||
return 3;
|
return 3;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'markdown',
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
|
@ -106,7 +117,11 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
||||||
const html = this.md.render(escapeMarkdown(this.content));
|
let contentToRender = this.content;
|
||||||
|
if (this.withMultiBreaks) {
|
||||||
|
contentToRender = contentToRender.replaceAll('\n\n', '\n \n');
|
||||||
|
}
|
||||||
|
const html = this.md.render(escapeMarkdown(contentToRender));
|
||||||
const safeHtml = xss(html, {
|
const safeHtml = xss(html, {
|
||||||
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
||||||
if (tag === 'img' && name === 'src') {
|
if (tag === 'img' && name === 'src') {
|
||||||
|
@ -214,6 +229,67 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h4, h5, h6 {
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--font-line-height-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--color-background-base);
|
||||||
|
padding: 0 var(--spacing-4xs);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code,li > code, p > code {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
margin: var(--spacing-2xl);
|
margin: var(--spacing-2xl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<label role="radio" tabindex="-1" :class="$style.container" aria-checked="true">
|
||||||
|
<input type="radio" tabindex="-1" autocomplete="off" :class="$style.input" :value="value">
|
||||||
|
<div :class="{[$style.button]: true, [$style.active]: active}" @click="$emit('click')">{{ label }}</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'n8n-radio-button',
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
display: inline-block;
|
||||||
|
outline: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.button:not(.active) {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
opacity: 0;
|
||||||
|
outline: 0;
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 26px;
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-base);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: var(--color-foreground-xlight);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
import N8nRadioButtons from './RadioButtons.vue';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/RadioButtons',
|
||||||
|
component: N8nRadioButtons,
|
||||||
|
argTypes: {
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: { default: '--color-background-xlight' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
onInput: action('input'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nRadioButtons,
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
`<n8n-radio-buttons v-model="val" v-bind="$props" @input="onInput">
|
||||||
|
</n8n-radio-buttons>`,
|
||||||
|
methods,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
val: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Example = Template.bind({});
|
||||||
|
Example.args = {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Test',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'World',
|
||||||
|
value: 'world',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hello',
|
||||||
|
value: 'hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<div role="radiogroup" :class="$style.radioGroup">
|
||||||
|
<RadioButton
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
v-bind="option"
|
||||||
|
:active="value === option.value"
|
||||||
|
@click="(e) => onClick(option.value, e)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import RadioButton from './RadioButton';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'n8n-radio-buttons',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
RadioButton,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick(value) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
|
||||||
|
.radioGroup {
|
||||||
|
display: inline-flex;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 0;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
padding: var(--spacing-5xs);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nRadioButtons from './RadioButtons.vue';
|
||||||
|
|
||||||
|
export default N8nRadioButtons;
|
|
@ -1,4 +1,8 @@
|
||||||
<template functional>
|
<template functional>
|
||||||
|
<div :class="{[$style.container]: true, [$style.withPrepend]: !!$slots.prepend}">
|
||||||
|
<div v-if="$slots.prepend" :class="$style.prepend">
|
||||||
|
<slot name="prepend" />
|
||||||
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="$options.components.ElSelect"
|
:is="$options.components.ElSelect"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
|
@ -19,6 +23,7 @@
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -121,4 +126,30 @@ export default {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withPrepend {
|
||||||
|
input {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepend {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
border: var(--border-base);
|
||||||
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--spacing-3xs);
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border-bottom-left-radius: var(--input-border-radius, var(--border-radius-base));
|
||||||
|
border-top-left-radius: var(--input-border-radius, var(--border-radius-base));
|
||||||
|
color: var(--color-text-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
238
packages/design-system/src/components/N8nSticky/Resize.vue
Normal file
238
packages/design-system/src/components/N8nSticky/Resize.vue
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.resize">
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="right" :class="[$style.resizer, $style.right]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="left" :class="[$style.resizer, $style.left]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top" :class="[$style.resizer, $style.top]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom" :class="[$style.resizer, $style.bottom]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-left" :class="[$style.resizer, $style.topLeft]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-right" :class="[$style.resizer, $style.topRight]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-left" :class="[$style.resizer, $style.bottomLeft]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-right" :class="[$style.resizer, $style.bottomRight]" />
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const cursorMap = {
|
||||||
|
right: 'ew-resize',
|
||||||
|
top: 'ns-resize',
|
||||||
|
bottom: 'ns-resize',
|
||||||
|
left: 'ew-resize',
|
||||||
|
'top-left': 'nw-resize',
|
||||||
|
'top-right' : 'ne-resize',
|
||||||
|
'bottom-left': 'sw-resize',
|
||||||
|
'bottom-right': 'se-resize',
|
||||||
|
};
|
||||||
|
|
||||||
|
function closestNumber(value: number, divisor: number): number {
|
||||||
|
let q = parseInt(value / divisor);
|
||||||
|
let n1 = divisor * q;
|
||||||
|
|
||||||
|
let n2 = (value * divisor) > 0 ?
|
||||||
|
(divisor * (q + 1)) : (divisor * (q - 1));
|
||||||
|
|
||||||
|
if (Math.abs(value - n1) < Math.abs(value - n2))
|
||||||
|
return n1;
|
||||||
|
|
||||||
|
return n2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSize(delta, min, virtual, gridSize): number {
|
||||||
|
const target = closestNumber(virtual, gridSize);
|
||||||
|
if (target >= min && virtual > 0) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'n8n-resize',
|
||||||
|
props: {
|
||||||
|
isResizingEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
gridSize: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dir: '',
|
||||||
|
dHeight: 0,
|
||||||
|
dWidth: 0,
|
||||||
|
vHeight: 0,
|
||||||
|
vWidth: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resizerMove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const targetResizer = e.target;
|
||||||
|
this.dir = targetResizer.dataset.dir;
|
||||||
|
document.body.style.cursor = cursorMap[this.dir];
|
||||||
|
|
||||||
|
this.x = e.pageX;
|
||||||
|
this.y = e.pageY;
|
||||||
|
this.dWidth = 0;
|
||||||
|
this.dHeight = 0;
|
||||||
|
this.vHeight = this.height;
|
||||||
|
this.vWidth = this.width;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', this.mouseMove);
|
||||||
|
window.addEventListener('mouseup', this.mouseUp);
|
||||||
|
this.$emit('resizestart');
|
||||||
|
},
|
||||||
|
mouseMove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
let dWidth = 0;
|
||||||
|
let dHeight = 0;
|
||||||
|
let top = false;
|
||||||
|
let left = false;
|
||||||
|
|
||||||
|
if (this.dir.includes('right')) {
|
||||||
|
dWidth = e.pageX - this.x;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('left')) {
|
||||||
|
dWidth = this.x - e.pageX;
|
||||||
|
left = true;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('top')) {
|
||||||
|
dHeight = this.y - e.pageY;
|
||||||
|
top = true;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('bottom')) {
|
||||||
|
dHeight = e.pageY - this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaWidth = (dWidth - this.dWidth) / this.scale;
|
||||||
|
const deltaHeight = (dHeight - this.dHeight) / this.scale;
|
||||||
|
|
||||||
|
this.vHeight = this.vHeight + deltaHeight;
|
||||||
|
this.vWidth = this.vWidth + deltaWidth;
|
||||||
|
const height = getSize(deltaHeight, this.minHeight, this.vHeight, this.gridSize);
|
||||||
|
const width = getSize(deltaWidth, this.minWidth, this.vWidth, this.gridSize);
|
||||||
|
|
||||||
|
const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
|
||||||
|
const dY = top && height !== this.height ? -1 * (height - this.height): 0;
|
||||||
|
|
||||||
|
this.$emit('resize', { height, width, dX, dY });
|
||||||
|
this.dHeight = dHeight;
|
||||||
|
this.dWidth = dWidth;
|
||||||
|
},
|
||||||
|
mouseUp(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.$emit('resizeend');
|
||||||
|
window.removeEventListener('mousemove', this.mouseMove);
|
||||||
|
window.removeEventListener('mouseup', this.mouseUp);
|
||||||
|
document.body.style.cursor = 'unset';
|
||||||
|
this.dir = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.resize {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topLeft {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
cursor: nw-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topRight {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: -3px;
|
||||||
|
right: -3px;
|
||||||
|
cursor: ne-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomLeft {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -3px;
|
||||||
|
left: -3px;
|
||||||
|
cursor: sw-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomRight {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -3px;
|
||||||
|
right: -3px;
|
||||||
|
cursor: se-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import N8nSticky from './Sticky.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Sticky',
|
||||||
|
component: N8nSticky,
|
||||||
|
argTypes: {
|
||||||
|
content: {
|
||||||
|
control: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
control: {
|
||||||
|
control: 'Boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
onInput: action('input'),
|
||||||
|
onResize: action('resize'),
|
||||||
|
onResizeEnd: action('resizeend'),
|
||||||
|
onResizeStart: action('resizestart'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nSticky,
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
'<n8n-sticky v-bind="$props" @resize="onResize" @resizeend="onResizeEnd" @resizeStart="onResizeStart" @input="onInput"></n8n-sticky>',
|
||||||
|
methods,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Sticky = Template.bind({});
|
||||||
|
Sticky.args = {
|
||||||
|
height: 160,
|
||||||
|
width: 150,
|
||||||
|
content: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
|
||||||
|
defaultText: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
|
||||||
|
minHeight: 80,
|
||||||
|
minWidth: 150,
|
||||||
|
readOnly: false,
|
||||||
|
};
|
253
packages/design-system/src/components/N8nSticky/Sticky.vue
Normal file
253
packages/design-system/src/components/N8nSticky/Sticky.vue
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{[$style.sticky]: true, [$style.clickable]: !isResizing}"
|
||||||
|
:style="styles"
|
||||||
|
@keydown.prevent
|
||||||
|
>
|
||||||
|
<resize
|
||||||
|
:isResizingEnabled="!readOnly"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:minHeight="minHeight"
|
||||||
|
:minWidth="minWidth"
|
||||||
|
:scale="scale"
|
||||||
|
:gridSize="gridSize"
|
||||||
|
@resizeend="onResizeEnd"
|
||||||
|
@resize="onResize"
|
||||||
|
@resizestart="onResizeStart"
|
||||||
|
>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="!editMode"
|
||||||
|
:class="$style.wrapper"
|
||||||
|
@dblclick.stop="onDoubleClick"
|
||||||
|
>
|
||||||
|
<n8n-markdown
|
||||||
|
theme="sticky"
|
||||||
|
:content="content"
|
||||||
|
:withMultiBreaks="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="editMode"
|
||||||
|
@click.stop
|
||||||
|
@mousedown.stop
|
||||||
|
@mouseup.stop
|
||||||
|
@keydown.esc="onInputBlur"
|
||||||
|
@keydown.stop
|
||||||
|
@wheel.stop
|
||||||
|
class="sticky-textarea"
|
||||||
|
:class="{'full-height': !shouldShowFooter}"
|
||||||
|
>
|
||||||
|
<n8n-input
|
||||||
|
:value="content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
@blur="onInputBlur"
|
||||||
|
@input="onInput"
|
||||||
|
ref="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
||||||
|
<n8n-text
|
||||||
|
size="xsmall"
|
||||||
|
aligh="right"
|
||||||
|
>
|
||||||
|
<span v-html="t('sticky.markdownHint')"></span>
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</resize>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import N8nInput from '../N8nInput';
|
||||||
|
import N8nMarkdown from '../N8nMarkdown';
|
||||||
|
import Resize from './Resize';
|
||||||
|
import N8nText from '../N8nText';
|
||||||
|
import Locale from '../../mixins/locale';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
export default mixins(Locale).extend({
|
||||||
|
name: 'n8n-sticky',
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 180,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 240,
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 150,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
gridSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: '0',
|
||||||
|
},
|
||||||
|
defaultText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
N8nInput,
|
||||||
|
N8nMarkdown,
|
||||||
|
Resize,
|
||||||
|
N8nText,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isResizing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resHeight(): number {
|
||||||
|
if (this.height < this.minHeight) {
|
||||||
|
return this.minHeight;
|
||||||
|
}
|
||||||
|
return this.height;
|
||||||
|
},
|
||||||
|
resWidth(): number {
|
||||||
|
if (this.width < this.minWidth) {
|
||||||
|
return this.minWidth;
|
||||||
|
}
|
||||||
|
return this.width;
|
||||||
|
},
|
||||||
|
styles() {
|
||||||
|
return {
|
||||||
|
height: this.resHeight + 'px',
|
||||||
|
width: this.resWidth + 'px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
shouldShowFooter() {
|
||||||
|
return this.resHeight > 100 && this.resWidth > 155;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDoubleClick() {
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.$emit('edit', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInputBlur(value) {
|
||||||
|
if (!this.isResizing) {
|
||||||
|
this.$emit('edit', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInput(value: string) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
onResize(values) {
|
||||||
|
this.$emit('resize', values);
|
||||||
|
},
|
||||||
|
onResizeEnd(resizeEnd) {
|
||||||
|
this.isResizing = false;
|
||||||
|
this.$emit('resizeend', resizeEnd);
|
||||||
|
},
|
||||||
|
onResizeStart() {
|
||||||
|
this.isResizing = true;
|
||||||
|
this.$emit('resizestart');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
editMode(newMode, prevMode) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (newMode && !prevMode && this.$refs.input && this.$refs.input.$refs && this.$refs.input.$refs.textarea) {
|
||||||
|
const textarea = this.$refs.input.$refs.textarea;
|
||||||
|
if (this.defaultText === this.content) {
|
||||||
|
textarea.select();
|
||||||
|
}
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.sticky {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-sticky-default-background);
|
||||||
|
border: 1px solid var(--color-sticky-default-border);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
padding: var(--spacing-2xs) var(--spacing-xs) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(180deg, var(--color-sticky-default-background), #fff5d600 0.01%, var(--color-sticky-default-background));
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: var(--spacing-5xs) var(--spacing-2xs) 0 var(--spacing-2xs);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sticky-textarea {
|
||||||
|
height: calc(100% - var(--spacing-l));
|
||||||
|
padding: var(--spacing-2xs) var(--spacing-2xs) 0 var(--spacing-2xs);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.el-textarea {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.el-textarea__inner {
|
||||||
|
height: 100%;
|
||||||
|
resize: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
height: calc(100% - var(--spacing-2xs));
|
||||||
|
}
|
||||||
|
</style>
|
3
packages/design-system/src/components/N8nSticky/index.js
Normal file
3
packages/design-system/src/components/N8nSticky/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Sticky from './Sticky.vue';
|
||||||
|
|
||||||
|
export default Sticky;
|
|
@ -0,0 +1,54 @@
|
||||||
|
import N8nTabs from './Tabs.vue';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Tabs',
|
||||||
|
component: N8nTabs,
|
||||||
|
argTypes: {
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: { default: '--color-background-xlight' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
onInput: action('input'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nTabs,
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
`<n8n-tabs v-model="val" v-bind="$props" @input="onInput">
|
||||||
|
</n8n-tabs>`,
|
||||||
|
methods,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
val: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Example = Template.bind({});
|
||||||
|
Example.args = {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Test',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Github',
|
||||||
|
value: 'github',
|
||||||
|
href: 'https://github.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
value: 'settings',
|
||||||
|
icon: 'cog',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
192
packages/design-system/src/components/N8nTabs/Tabs.vue
Normal file
192
packages/design-system/src/components/N8nTabs/Tabs.vue
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div :class="$style.back" v-if="scrollPosition > 0" @click="scrollLeft">
|
||||||
|
<n8n-icon icon="chevron-left" size="small" />
|
||||||
|
</div>
|
||||||
|
<div :class="$style.next" v-if="canScrollRight" @click="scrollRight">
|
||||||
|
<n8n-icon icon="chevron-right" size="small" />
|
||||||
|
</div>
|
||||||
|
<div ref="tabs" :class="$style.tabs">
|
||||||
|
<div v-for="option in options" :key="option.value" :class="{ [$style.alignRight]: option.align === 'right' }">
|
||||||
|
<a
|
||||||
|
v-if="option.href"
|
||||||
|
target="_blank"
|
||||||
|
:href="option.href"
|
||||||
|
:class="[$style.link, $style.tab]"
|
||||||
|
@click="handleTabClick"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{ option.label }}
|
||||||
|
<span :class="$style.external"><n8n-icon icon="external-link-alt" size="small" /></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="{ [$style.tab]: true, [$style.activeTab]: value === option.value }"
|
||||||
|
@click="() => handleTabClick(option.value)"
|
||||||
|
>
|
||||||
|
<n8n-icon v-if="option.icon" :icon="option.icon" size="medium" />
|
||||||
|
<span v-if="option.label">{{ option.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import N8nIcon from '../N8nIcon';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'N8nTabs',
|
||||||
|
components: {
|
||||||
|
N8nIcon,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const container = this.$refs.tabs;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('scroll', (e) => {
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
this.scrollPosition = e.srcElement.scrollLeft;
|
||||||
|
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(container);
|
||||||
|
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.resizeObserver.disconnect();
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
scrollPosition: 0,
|
||||||
|
canScrollRight: false,
|
||||||
|
resizeObserver: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabClick(tab: string) {
|
||||||
|
this.$emit('input', tab);
|
||||||
|
},
|
||||||
|
scrollLeft() {
|
||||||
|
this.scroll(-50);
|
||||||
|
},
|
||||||
|
scrollRight() {
|
||||||
|
this.scroll(50);
|
||||||
|
},
|
||||||
|
scroll(left: number) {
|
||||||
|
const container = this.$refs.tabs;
|
||||||
|
if (container) {
|
||||||
|
container.scrollBy({ left, top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
height: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
overflow-x: scroll;
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: block;
|
||||||
|
padding: 0 var(--spacing-s) var(--spacing-2xs) var(--spacing-s);
|
||||||
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeTab {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-bottom: var(--color-primary) 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignRight {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
|
.external {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
z-index: 1;
|
||||||
|
height: 24px;
|
||||||
|
width: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
composes: tab;
|
||||||
|
composes: button;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
composes: tab;
|
||||||
|
composes: button;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
3
packages/design-system/src/components/N8nTabs/index.js
Normal file
3
packages/design-system/src/components/N8nTabs/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nTabs from './Tabs.vue';
|
||||||
|
|
||||||
|
export default N8nTabs;
|
|
@ -12,6 +12,7 @@ import Switch from 'element-ui/lib/switch';
|
||||||
import Select from 'element-ui/lib/select';
|
import Select from 'element-ui/lib/select';
|
||||||
import Option from 'element-ui/lib/option';
|
import Option from 'element-ui/lib/option';
|
||||||
import OptionGroup from 'element-ui/lib/option-group';
|
import OptionGroup from 'element-ui/lib/option-group';
|
||||||
|
import Pagination from 'element-ui/lib/pagination';
|
||||||
import ButtonGroup from 'element-ui/lib/button-group';
|
import ButtonGroup from 'element-ui/lib/button-group';
|
||||||
import Table from 'element-ui/lib/table';
|
import Table from 'element-ui/lib/table';
|
||||||
import TableColumn from 'element-ui/lib/table-column';
|
import TableColumn from 'element-ui/lib/table-column';
|
||||||
|
@ -29,6 +30,7 @@ import Loading from 'element-ui/lib/loading';
|
||||||
import MessageBox from 'element-ui/lib/message-box';
|
import MessageBox from 'element-ui/lib/message-box';
|
||||||
import Message from 'element-ui/lib/message';
|
import Message from 'element-ui/lib/message';
|
||||||
import Notification from 'element-ui/lib/notification';
|
import Notification from 'element-ui/lib/notification';
|
||||||
|
import Popover from 'element-ui/lib/popover';
|
||||||
import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
|
import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
|
||||||
|
|
||||||
import N8nActionBox from './N8nActionBox';
|
import N8nActionBox from './N8nActionBox';
|
||||||
|
@ -52,10 +54,13 @@ import N8nMenu from './N8nMenu';
|
||||||
import N8nMenuItem from './N8nMenuItem';
|
import N8nMenuItem from './N8nMenuItem';
|
||||||
import N8nLink from './N8nLink';
|
import N8nLink from './N8nLink';
|
||||||
import N8nOption from './N8nOption';
|
import N8nOption from './N8nOption';
|
||||||
|
import N8nRadioButtons from './N8nRadioButtons';
|
||||||
import N8nSelect from './N8nSelect';
|
import N8nSelect from './N8nSelect';
|
||||||
import N8nSpinner from './N8nSpinner';
|
import N8nSpinner from './N8nSpinner';
|
||||||
|
import N8nSticky from './N8nSticky';
|
||||||
import N8nSquareButton from './N8nSquareButton';
|
import N8nSquareButton from './N8nSquareButton';
|
||||||
import N8nTags from './N8nTags';
|
import N8nTags from './N8nTags';
|
||||||
|
import N8nTabs from './N8nTabs';
|
||||||
import N8nTag from './N8nTag';
|
import N8nTag from './N8nTag';
|
||||||
import N8nText from './N8nText';
|
import N8nText from './N8nText';
|
||||||
import N8nTooltip from './N8nTooltip';
|
import N8nTooltip from './N8nTooltip';
|
||||||
|
@ -86,9 +91,12 @@ export {
|
||||||
N8nMenu,
|
N8nMenu,
|
||||||
N8nMenuItem,
|
N8nMenuItem,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
N8nRadioButtons,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nSticky,
|
||||||
N8nSquareButton,
|
N8nSquareButton,
|
||||||
|
N8nTabs,
|
||||||
N8nTags,
|
N8nTags,
|
||||||
N8nTag,
|
N8nTag,
|
||||||
N8nText,
|
N8nText,
|
||||||
|
@ -110,6 +118,7 @@ export {
|
||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
OptionGroup,
|
OptionGroup,
|
||||||
|
Pagination,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Table,
|
Table,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
|
@ -128,6 +137,7 @@ export {
|
||||||
Message,
|
Message,
|
||||||
Notification,
|
Notification,
|
||||||
CollapseTransition,
|
CollapseTransition,
|
||||||
|
Popover,
|
||||||
|
|
||||||
locale,
|
locale,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export function addTargetBlank(html: string) {
|
export function addTargetBlank(html: string) {
|
||||||
return html.includes('href=')
|
return html && html.includes('href=')
|
||||||
? html.replace(/href=/g, 'target="_blank" href=')
|
? html.replace(/href=/g, 'target="_blank" href=')
|
||||||
: html;
|
: html;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,5 @@ export default {
|
||||||
config.minimum > 1 ? 's' : ''
|
config.minimum > 1 ? 's' : ''
|
||||||
}`),
|
}`),
|
||||||
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
|
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
|
||||||
|
"sticky.markdownHint": `You can style with <a href="https://docs.n8n.io/workflows/sticky-notes/" target="_blank">Markdown</a>`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -154,3 +154,29 @@ import ColorCircles from './ColorCircles.vue';
|
||||||
}}
|
}}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story name="json">
|
||||||
|
{{
|
||||||
|
template: `<color-circles :colors="['--color-json-default', '--color-json-null', '--color-json-boolean', '--color-json-number', '--color-json-string', '--color-json-key', '--color-json-brackets', '--color-json-brackets-hover', '--color-json-line', '--color-json-highlight']" />`,
|
||||||
|
components: {
|
||||||
|
ColorCircles,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Sticky
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story name="sticky">
|
||||||
|
{{
|
||||||
|
template: `<color-circles :colors="['--color-sticky-default-background', '--color-sticky-default-border']" />`,
|
||||||
|
components: {
|
||||||
|
ColorCircles,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ export const escapeMarkdown = (html: string | undefined): string => {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const escaped = html.replace(/</g, "<").replace(/>/g, ">");
|
const escaped = html.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
// unescape greater than quotes at start of line
|
// unescape greater than quotes at start of line
|
||||||
const withQuotes = escaped.replace(/^((\s)*(>)+)+\s*/gm, (matches) => {
|
const withQuotes = escaped.replace(/^((\s)*(>)+)+\s*/gm, (matches) => {
|
||||||
return matches.replace(/>/g, '>');
|
return matches.replace(/>/g, '>');
|
||||||
|
|
|
@ -70,13 +70,6 @@
|
||||||
var(--color-secondary-l)
|
var(--color-secondary-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
--color-secondary-tint-1-l: 92%;
|
|
||||||
--color-secondary-tint-1: hsl(
|
|
||||||
var(--color-secondary-h),
|
|
||||||
var(--color-secondary-s),
|
|
||||||
var(--color-secondary-tint-1-l)
|
|
||||||
);
|
|
||||||
|
|
||||||
--color-success-h: 150.4;
|
--color-success-h: 150.4;
|
||||||
--color-success-s: 60%;
|
--color-success-s: 60%;
|
||||||
--color-success-l: 40.4%;
|
--color-success-l: 40.4%;
|
||||||
|
@ -285,8 +278,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
--color-background-light-h: 220;
|
--color-background-light-h: 220;
|
||||||
--color-background-light-s: 27.3%;
|
--color-background-light-s: 60%;
|
||||||
--color-background-light-l: 97.8%;
|
--color-background-light-l: 99%;
|
||||||
--color-background-light: hsl(
|
--color-background-light: hsl(
|
||||||
var(--color-background-light-h),
|
var(--color-background-light-h),
|
||||||
var(--color-background-light-s),
|
var(--color-background-light-s),
|
||||||
|
@ -329,6 +322,35 @@
|
||||||
var(--color-canvas-background-l)
|
var(--color-canvas-background-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--color-json-default: #5045A1;
|
||||||
|
--color-json-null: var(--color-danger);
|
||||||
|
--color-json-boolean: #1d8ce0;
|
||||||
|
--color-json-number: #1d8ce0;
|
||||||
|
--color-json-string: #5045A1;
|
||||||
|
--color-json-key: var(--color-text-dark);
|
||||||
|
--color-json-brackets: var(--color-text-dark);
|
||||||
|
--color-json-brackets-hover: #1890ff;
|
||||||
|
--color-json-line: #bfcbd9;
|
||||||
|
--color-json-highlight: #E2E5EE;
|
||||||
|
|
||||||
|
--color-sticky-default-background-h: 46;
|
||||||
|
--color-sticky-default-background-s: 100%;
|
||||||
|
--color-sticky-default-background-l: 92%;
|
||||||
|
--color-sticky-default-background: hsl(
|
||||||
|
var(--color-sticky-default-background-h),
|
||||||
|
var(--color-sticky-default-background-s),
|
||||||
|
var(--color-sticky-default-background-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
--color-sticky-default-border-h: 43;
|
||||||
|
--color-sticky-default-border-s: 75%;
|
||||||
|
--color-sticky-default-border-l: 80%;
|
||||||
|
--color-sticky-default-border: hsl(
|
||||||
|
var(--color-sticky-default-border-h),
|
||||||
|
var(--color-sticky-default-border-s),
|
||||||
|
var(--color-sticky-default-border-l)
|
||||||
|
);
|
||||||
|
|
||||||
--border-radius-xlarge: 12px;
|
--border-radius-xlarge: 12px;
|
||||||
--border-radius-large: 8px;
|
--border-radius-large: 8px;
|
||||||
--border-radius-base: 4px;
|
--border-radius-base: 4px;
|
||||||
|
|
|
@ -777,7 +777,7 @@ $table-fixed-box-shadow: 0 0 10px rgba(0, 0, 0, 0.12);
|
||||||
/* Pagination
|
/* Pagination
|
||||||
-------------------------- */
|
-------------------------- */
|
||||||
/// fontSize||Font|1
|
/// fontSize||Font|1
|
||||||
$pagination-font-size: 13px;
|
$pagination-font-size: var(--font-size-2xs);
|
||||||
/// color||Color|0
|
/// color||Color|0
|
||||||
$pagination-background-color: $color-white;
|
$pagination-background-color: $color-white;
|
||||||
/// color||Color|0
|
/// color||Color|0
|
||||||
|
@ -799,7 +799,7 @@ $pagination-hover-color: var(--color-primary);
|
||||||
/* Popup
|
/* Popup
|
||||||
-------------------------- */
|
-------------------------- */
|
||||||
/// color||Color|0
|
/// color||Color|0
|
||||||
$popup-modal-background-color: hsla(247,14%, 70%, 0.75);
|
$popup-modal-background-color: hsla(247,14%, 30%, 0.75);
|
||||||
/// opacity||Other|1
|
/// opacity||Other|1
|
||||||
$popup-modal-opacity: 0.65;
|
$popup-modal-opacity: 0.65;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
@use "./tokens.dark.scss" as dark;
|
@use "./tokens.dark.scss" as dark;
|
||||||
@use "./reset.scss";
|
@use "./reset.scss";
|
||||||
@use "./base.scss";
|
@use "./base.scss";
|
||||||
// @use "./pagination.scss";
|
@use "./pagination.scss";
|
||||||
@use "./dialog.scss";
|
@use "./dialog.scss";
|
||||||
// @use "./autocomplete.scss";
|
// @use "./autocomplete.scss";
|
||||||
@use "./dropdown.scss";
|
@use "./dropdown.scss";
|
||||||
|
|
|
@ -193,11 +193,11 @@
|
||||||
.btn-prev,
|
.btn-prev,
|
||||||
.btn-next,
|
.btn-next,
|
||||||
.el-pager li {
|
.el-pager li {
|
||||||
margin: 0 5px;
|
margin: 0 1px;
|
||||||
background-color: var.$color-info-lighter;
|
color: var(--color-text-base);
|
||||||
color: var(--color-text-dark);
|
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
border-radius: 2px;
|
border-radius: var(--border-radius-base);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: var(--color-text-lighter);
|
color: var(--color-text-lighter);
|
||||||
|
@ -215,12 +215,14 @@
|
||||||
|
|
||||||
.el-pager li:not(.disabled) {
|
.el-pager li:not(.disabled) {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var.$pagination-hover-color;
|
color: var(--color-primary);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
border: 1px solid var(--color-foreground-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--color-primary);
|
border: 1px solid var(--color-primary);
|
||||||
color: var.$color-white;
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,7 +254,6 @@
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
background: var.$pagination-background-color;
|
background: var.$pagination-background-color;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
|
||||||
font-size: var.$pagination-font-size;
|
font-size: var.$pagination-font-size;
|
||||||
min-width: var.$pagination-button-width;
|
min-width: var.$pagination-button-width;
|
||||||
height: var.$pagination-button-height;
|
height: var.$pagination-button-height;
|
||||||
|
@ -261,6 +262,9 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&.btn-quicknext,
|
&.btn-quicknext,
|
||||||
&.btn-quickprev {
|
&.btn-quickprev {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.138.0",
|
"version": "0.141.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -25,14 +25,15 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.0",
|
"@fontsource/open-sans": "^4.5.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"luxon": "^2.3.0",
|
"luxon": "^2.3.0",
|
||||||
"n8n-design-system": "~0.16.0",
|
|
||||||
"monaco-editor": "^0.29.1",
|
"monaco-editor": "^0.29.1",
|
||||||
|
"n8n-design-system": "~0.18.0",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-fragment": "^1.5.2",
|
"vue-fragment": "^1.5.2",
|
||||||
"vue2-boring-avatars": "0.3.4",
|
|
||||||
"vue-i18n": "^8.26.7",
|
"vue-i18n": "^8.26.7",
|
||||||
|
"vue2-boring-avatars": "0.3.4",
|
||||||
"xss": "^1.0.10"
|
"xss": "^1.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.set": "^4.3.2",
|
"lodash.set": "^4.3.2",
|
||||||
"n8n-workflow": "~0.94.0",
|
"n8n-workflow": "~0.97.0",
|
||||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
|
|
|
@ -28,10 +28,13 @@ import { showMessage } from './components/mixins/showMessage';
|
||||||
import { IUser } from './Interface';
|
import { IUser } from './Interface';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { userHelpers } from './components/mixins/userHelpers';
|
import { userHelpers } from './components/mixins/userHelpers';
|
||||||
|
import { addHeaders, loadLanguage } from './plugins/i18n';
|
||||||
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
showMessage,
|
showMessage,
|
||||||
userHelpers,
|
userHelpers,
|
||||||
|
restApi,
|
||||||
).extend({
|
).extend({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
|
@ -42,6 +45,9 @@ export default mixins(
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('settings', ['isHiringBannerEnabled', 'isTemplatesEnabled', 'isTemplatesEndpointReachable', 'isUserManagementEnabled', 'showSetupPage']),
|
...mapGetters('settings', ['isHiringBannerEnabled', 'isTemplatesEnabled', 'isTemplatesEndpointReachable', 'isUserManagementEnabled', 'showSetupPage']),
|
||||||
...mapGetters('users', ['currentUser']),
|
...mapGetters('users', ['currentUser']),
|
||||||
|
defaultLocale (): string {
|
||||||
|
return this.$store.getters.defaultLocale;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -79,7 +85,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logHiringBanner() {
|
logHiringBanner() {
|
||||||
if (!this.isHiringBannerEnabled && this.$route.name !== VIEWS.DEMO) {
|
if (this.isHiringBannerEnabled && this.$route.name !== VIEWS.DEMO) {
|
||||||
console.log(HIRING_BANNER); // eslint-disable-line no-console
|
console.log(HIRING_BANNER); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,8 +149,8 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.logHiringBanner();
|
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
this.logHiringBanner();
|
||||||
this.authenticate();
|
this.authenticate();
|
||||||
this.redirectIfNecessary();
|
this.redirectIfNecessary();
|
||||||
|
|
||||||
|
@ -152,6 +158,11 @@ export default mixins(
|
||||||
|
|
||||||
this.trackPage();
|
this.trackPage();
|
||||||
this.$externalHooks().run('app.mount');
|
this.$externalHooks().run('app.mount');
|
||||||
|
|
||||||
|
if (this.defaultLocale !== 'en') {
|
||||||
|
const headers = await this.restApi().getNodeTranslationHeaders();
|
||||||
|
if (headers) addHeaders(headers, this.defaultLocale);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
$route(route) {
|
$route(route) {
|
||||||
|
@ -160,6 +171,9 @@ export default mixins(
|
||||||
|
|
||||||
this.trackPage();
|
this.trackPage();
|
||||||
},
|
},
|
||||||
|
defaultLocale(newLocale) {
|
||||||
|
loadLanguage(newLocale);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -746,6 +746,10 @@ export interface ITemplatesNode extends IVersionNode {
|
||||||
categories?: ITemplatesCategory[];
|
categories?: ITemplatesCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INodeMetadata {
|
||||||
|
parametersLastUpdatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRootState {
|
export interface IRootState {
|
||||||
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
||||||
activeWorkflows: string[];
|
activeWorkflows: string[];
|
||||||
|
@ -783,6 +787,7 @@ export interface IRootState {
|
||||||
workflow: IWorkflowDb;
|
workflow: IWorkflowDb;
|
||||||
sidebarMenuItems: IMenuItem[];
|
sidebarMenuItems: IMenuItem[];
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
nodeMetadata: {[nodeName: string]: INodeMetadata};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialTypeMap {
|
export interface ICredentialTypeMap {
|
||||||
|
@ -958,3 +963,11 @@ export type IFormBoxConfig = {
|
||||||
redirectLink?: string;
|
redirectLink?: string;
|
||||||
redirectText?: string;
|
redirectText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ITab {
|
||||||
|
value: string | number;
|
||||||
|
label?: string;
|
||||||
|
href?: string;
|
||||||
|
icon?: string;
|
||||||
|
align?: 'right';
|
||||||
|
}
|
||||||
|
|
|
@ -1,40 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible="!!node"
|
:visible="(!!node || renaming) && !isActiveStickyNode"
|
||||||
:before-close="close"
|
:before-close="close"
|
||||||
:custom-class="`classic data-display-wrapper`"
|
:show-close="false"
|
||||||
|
custom-class="data-display-wrapper"
|
||||||
width="85%"
|
width="85%"
|
||||||
append-to-body
|
append-to-body
|
||||||
@opened="showDocumentHelp = true"
|
|
||||||
>
|
>
|
||||||
<div class="data-display" >
|
<n8n-tooltip placement="bottom-start" :value="showTriggerWaitingWarning" :disabled="!showTriggerWaitingWarning" :manual="true">
|
||||||
<NodeSettings @valueChanged="valueChanged" />
|
<div slot="content" :class="$style.triggerWarning">{{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}</div>
|
||||||
<RunData />
|
<div :class="$style.backToCanvas" @click="close">
|
||||||
|
<n8n-icon icon="arrow-left" color="text-xlight" size="medium" />
|
||||||
|
<n8n-text color="text-xlight" size="medium" :bold="true">{{ $locale.baseText('ndv.backToCanvas') }}</n8n-text>
|
||||||
|
</div>
|
||||||
|
</n8n-tooltip>
|
||||||
|
|
||||||
|
<div class="data-display" v-if="node" >
|
||||||
|
<NodeSettings :eventBus="settingsEventBus" @valueChanged="valueChanged" @execute="onNodeExecute" />
|
||||||
|
<RunData @openSettings="openSettings" />
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
|
||||||
<div v-if="nodeType && showDocumentHelp" class="doc-help-wrapper">
|
|
||||||
<svg id="help-logo" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>{{ $locale.baseText('dataDisplay.nodeDocumentation') }}</title>
|
|
||||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
|
||||||
<g transform="translate(1117.000000, 825.000000)">
|
|
||||||
<g transform="translate(10.000000, 11.000000)">
|
|
||||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
|
||||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
|
||||||
</g>
|
|
||||||
<rect x="0" y="0" width="18" height="18"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class="text">
|
|
||||||
{{ $locale.baseText('dataDisplay.needHelp') }} <n8n-link size="small" :to="documentationUrl" :bold="true" @click="onDocumentationUrlClick">{{ $locale.baseText('dataDisplay.openDocumentationFor', { interpolate: { nodeTypeDisplayName: nodeType.displayName } }) }}</n8n-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -55,6 +39,9 @@ import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import RunData from '@/components/RunData.vue';
|
import RunData from '@/components/RunData.vue';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { STICKY_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
name: 'DataDisplay',
|
name: 'DataDisplay',
|
||||||
|
@ -62,23 +49,24 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
NodeSettings,
|
NodeSettings,
|
||||||
RunData,
|
RunData,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
renaming: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
basePath: this.$store.getters.getBaseUrl,
|
settingsEventBus: new Vue(),
|
||||||
showDocumentHelp: false,
|
triggerWaitingWarningEnabled: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
documentationUrl (): string {
|
...mapGetters(['executionWaitingForWebhook']),
|
||||||
if (!this.nodeType) {
|
workflowRunning (): boolean {
|
||||||
return '';
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
}
|
},
|
||||||
|
showTriggerWaitingWarning(): boolean {
|
||||||
if (this.nodeType.documentationUrl && this.nodeType.documentationUrl.startsWith('http')) {
|
return this.triggerWaitingWarningEnabled && !!this.nodeType && !this.nodeType.group.includes('trigger') && this.workflowRunning && this.executionWaitingForWebhook;
|
||||||
return this.nodeType.documentationUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'https://docs.n8n.io/nodes/' + (this.nodeType.documentationUrl || this.nodeType.name) + '?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=' + this.nodeType.name;
|
|
||||||
},
|
},
|
||||||
node (): INodeUi {
|
node (): INodeUi {
|
||||||
return this.$store.getters.activeNode;
|
return this.$store.getters.activeNode;
|
||||||
|
@ -89,19 +77,34 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
isActiveStickyNode(): boolean {
|
||||||
|
return !!this.$store.getters.activeNode && this.$store.getters.activeNode.type === STICKY_NODE_TYPE;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
node (node, oldNode) {
|
node (node, oldNode) {
|
||||||
if(node && !oldNode) {
|
if(node && !oldNode && !this.isActiveStickyNode) {
|
||||||
|
this.triggerWaitingWarningEnabled = false;
|
||||||
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
||||||
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
||||||
}
|
}
|
||||||
if (window.top) {
|
if (window.top && !this.isActiveStickyNode) {
|
||||||
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
|
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onNodeExecute() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.node || !this.workflowRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.triggerWaitingWarningEnabled = true;
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
openSettings() {
|
||||||
|
this.settingsEventBus.$emit('openSettings');
|
||||||
|
},
|
||||||
valueChanged (parameterData: IUpdateInformation) {
|
valueChanged (parameterData: IUpdateInformation) {
|
||||||
this.$emit('valueChanged', parameterData);
|
this.$emit('valueChanged', parameterData);
|
||||||
},
|
},
|
||||||
|
@ -110,12 +113,9 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
},
|
},
|
||||||
close () {
|
close () {
|
||||||
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
||||||
this.showDocumentHelp = false;
|
this.triggerWaitingWarningEnabled = false;
|
||||||
this.$store.commit('setActiveNode', null);
|
this.$store.commit('setActiveNode', null);
|
||||||
},
|
},
|
||||||
onDocumentationUrlClick () {
|
|
||||||
this.$externalHooks().run('dataDisplay.onDocumentationUrlClick', { nodeType: this.nodeType, documentationUrl: this.documentationUrl });
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.data-display-wrapper {
|
.data-display-wrapper {
|
||||||
height: 85%;
|
height: 85%;
|
||||||
|
margin-top: 48px !important;
|
||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
@ -145,41 +146,6 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-help-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
transition-delay: 2s;
|
|
||||||
background-color: #fff;
|
|
||||||
margin-top: 1%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid #DCDFE6;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
box-shadow: 0 2px 7px 0 rgba(0,0,0,0.15);
|
|
||||||
min-width: 319px;
|
|
||||||
height: 40px;
|
|
||||||
float: right;
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-right: 12px;
|
|
||||||
|
|
||||||
#help-logo {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-left: 5px;
|
|
||||||
flex: 9;
|
|
||||||
font-family: "Open Sans";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 17px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active, .fade-enter-to, .fade-leave-active {
|
.fade-enter-active, .fade-enter-to, .fade-leave-active {
|
||||||
transition: all .75s ease;
|
transition: all .75s ease;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -189,3 +155,30 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.triggerWarning {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backToCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $--breakpoint-lg) {
|
||||||
|
.backToCanvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -90,8 +90,11 @@ export default mixins(showMessage).extend({
|
||||||
return this.userToDelete && !this.userToDelete.firstName;
|
return this.userToDelete && !this.userToDelete.firstName;
|
||||||
},
|
},
|
||||||
title(): string {
|
title(): string {
|
||||||
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email);
|
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email) || '';
|
||||||
return this.$locale.baseText('settings.users.deleteUser', { interpolate: { user }});
|
return this.$locale.baseText(
|
||||||
|
'settings.users.deleteUser',
|
||||||
|
{ interpolate: { user }},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
enabled(): boolean {
|
enabled(): boolean {
|
||||||
if (this.isPending) {
|
if (this.isPending) {
|
||||||
|
@ -138,7 +141,10 @@ export default mixins(showMessage).extend({
|
||||||
if (this.transferId) {
|
if (this.transferId) {
|
||||||
const getUserById = this.$store.getters['users/getUserById'];
|
const getUserById = this.$store.getters['users/getUserById'];
|
||||||
const transferUser: IUser = getUserById(this.transferId);
|
const transferUser: IUser = getUserById(this.transferId);
|
||||||
message = this.$locale.baseText('settings.users.transferredToUser', { interpolate: { user: transferUser.fullName }});
|
message = this.$locale.baseText(
|
||||||
|
'settings.users.transferredToUser',
|
||||||
|
{ interpolate: { user: transferUser.fullName || '' }},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="static-text-wrapper">
|
|
||||||
<span v-show="!editActive" :title="$locale.baseText('displayWithChange.clickToChange')">
|
|
||||||
<span class="static-text" @mousedown="startEdit">{{currentValue}}</span>
|
|
||||||
</span>
|
|
||||||
<span v-show="editActive">
|
|
||||||
<input class="edit-field" ref="inputField" type="text" v-model="newValue" @keydown.enter.stop.prevent="setValue" @keydown.escape.stop.prevent="cancelEdit" @keydown.stop="noOp" @blur="cancelEdit" />
|
|
||||||
<font-awesome-icon icon="times" @mousedown="cancelEdit" class="icons clickable" :title="$locale.baseText('displayWithChange.cancelEdit')" />
|
|
||||||
<font-awesome-icon icon="check" @mousedown="setValue" class="icons clickable" :title="$locale.baseText('displayWithChange.setValue')" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
|
||||||
import { INodeUi } from '@/Interface';
|
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
|
||||||
|
|
||||||
export default mixins(genericHelpers).extend({
|
|
||||||
name: 'DisplayWithChange',
|
|
||||||
props: {
|
|
||||||
keyName: String,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
node (): INodeUi {
|
|
||||||
return this.$store.getters.activeNode;
|
|
||||||
},
|
|
||||||
currentValue (): string {
|
|
||||||
const getDescendantProp = (obj: object, path: string): string => {
|
|
||||||
// @ts-ignore
|
|
||||||
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.keyName === 'name' && this.node.type.startsWith('n8n-nodes-base.')) {
|
|
||||||
const shortNodeType = this.$locale.shortNodeType(this.node.type);
|
|
||||||
|
|
||||||
return this.$locale.headerText({
|
|
||||||
key: `headers.${shortNodeType}.displayName`,
|
|
||||||
fallback: getDescendantProp(this.node, this.keyName),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDescendantProp(this.node, this.keyName);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
currentValue (val) {
|
|
||||||
// Deactivate when the data to edit changes
|
|
||||||
// (like when a different node gets selected)
|
|
||||||
this.editActive = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: () => {
|
|
||||||
return {
|
|
||||||
editActive: false,
|
|
||||||
newValue: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
noOp () {},
|
|
||||||
startEdit () {
|
|
||||||
if (this.isReadOnly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.editActive = true;
|
|
||||||
this.newValue = this.currentValue;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
(this.$refs.inputField as HTMLInputElement).focus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
cancelEdit () {
|
|
||||||
this.editActive = false;
|
|
||||||
},
|
|
||||||
setValue () {
|
|
||||||
const sendData = {
|
|
||||||
value: this.newValue,
|
|
||||||
name: this.keyName,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('valueChanged', sendData);
|
|
||||||
this.editActive = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
|
|
||||||
.static-text-wrapper {
|
|
||||||
line-height: 1.4em;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
.static-text {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-bottom: 1px dashed #555;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.edit-field {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1em;
|
|
||||||
color: #555;
|
|
||||||
border-bottom: 1px dashed #555;
|
|
||||||
width: calc(100% - 130px);
|
|
||||||
}
|
|
||||||
&.edit-field:focus {
|
|
||||||
outline-offset: unset;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
margin-left: 0.6em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -157,7 +157,7 @@ export default mixins(
|
||||||
padding: 1em 0 0.5em 1.8em;
|
padding: 1em 0 0.5em 1.8em;
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
|
|
||||||
background-color: $--custom-window-sidebar-top;
|
background-color: var(--color-background-base);
|
||||||
color: #555;
|
color: #555;
|
||||||
border-bottom: 1px solid $--color-primary;
|
border-bottom: 1px solid $--color-primary;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
|
@ -106,7 +106,10 @@ export default mixins(showMessage).extend({
|
||||||
},
|
},
|
||||||
buttonLabel(): string {
|
buttonLabel(): string {
|
||||||
if (this.emailsCount > 1) {
|
if (this.emailsCount > 1) {
|
||||||
return this.$locale.baseText('settings.users.inviteXUser', { interpolate: { count: this.emailsCount }});
|
return this.$locale.baseText(
|
||||||
|
'settings.users.inviteXUser',
|
||||||
|
{ interpolate: { count: this.emailsCount.toString() }},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$locale.baseText('settings.users.inviteUser');
|
return this.$locale.baseText('settings.users.inviteUser');
|
||||||
|
|
|
@ -26,13 +26,13 @@
|
||||||
</span>
|
</span>
|
||||||
{{ $locale.baseText('executionDetails.of') }}
|
{{ $locale.baseText('executionDetails.of') }}
|
||||||
<span class="primary-color clickable" :title="$locale.baseText('executionDetails.openWorkflow')">
|
<span class="primary-color clickable" :title="$locale.baseText('executionDetails.openWorkflow')">
|
||||||
<WorkflowNameShort :name="workflowName">
|
<ShortenName :name="workflowName">
|
||||||
<template v-slot="{ shortenedName }">
|
<template v-slot="{ shortenedName }">
|
||||||
<span @click="openWorkflow(workflowExecution.workflowId)">
|
<span @click="openWorkflow(workflowExecution.workflowId)">
|
||||||
"{{ shortenedName }}"
|
"{{ shortenedName }}"
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</WorkflowNameShort>
|
</ShortenName>
|
||||||
</span>
|
</span>
|
||||||
{{ $locale.baseText('executionDetails.workflow') }}
|
{{ $locale.baseText('executionDetails.workflow') }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -47,13 +47,13 @@ import { IExecutionResponse } from "../../../Interface";
|
||||||
|
|
||||||
import { titleChange } from "@/components/mixins/titleChange";
|
import { titleChange } from "@/components/mixins/titleChange";
|
||||||
|
|
||||||
import WorkflowNameShort from "@/components/WorkflowNameShort.vue";
|
import ShortenName from "@/components/ShortenName.vue";
|
||||||
import ReadOnly from "@/components/MainHeader/ExecutionDetails/ReadOnly.vue";
|
import ReadOnly from "@/components/MainHeader/ExecutionDetails/ReadOnly.vue";
|
||||||
|
|
||||||
export default mixins(titleChange).extend({
|
export default mixins(titleChange).extend({
|
||||||
name: "ExecutionDetails",
|
name: "ExecutionDetails",
|
||||||
components: {
|
components: {
|
||||||
WorkflowNameShort,
|
ShortenName,
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="container" v-if="workflowName">
|
<div class="container" v-if="workflowName">
|
||||||
<BreakpointsObserver :valueXS="15" :valueSM="25" :valueMD="50" class="name-container">
|
<BreakpointsObserver :valueXS="15" :valueSM="25" :valueMD="50" class="name-container">
|
||||||
<template v-slot="{ value }">
|
<template v-slot="{ value }">
|
||||||
<WorkflowNameShort
|
<ShortenName
|
||||||
:name="workflowName"
|
:name="workflowName"
|
||||||
:limit="value"
|
:limit="value"
|
||||||
:custom="true"
|
:custom="true"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
class="name"
|
class="name"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</WorkflowNameShort>
|
</ShortenName>
|
||||||
</template>
|
</template>
|
||||||
</BreakpointsObserver>
|
</BreakpointsObserver>
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ import mixins from "vue-typed-mixins";
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters } from "vuex";
|
||||||
import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants";
|
import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants";
|
||||||
|
|
||||||
import WorkflowNameShort from "@/components/WorkflowNameShort.vue";
|
import ShortenName from "@/components/ShortenName.vue";
|
||||||
import TagsContainer from "@/components/TagsContainer.vue";
|
import TagsContainer from "@/components/TagsContainer.vue";
|
||||||
import PushConnectionTracker from "@/components/PushConnectionTracker.vue";
|
import PushConnectionTracker from "@/components/PushConnectionTracker.vue";
|
||||||
import WorkflowActivator from "@/components/WorkflowActivator.vue";
|
import WorkflowActivator from "@/components/WorkflowActivator.vue";
|
||||||
|
@ -105,7 +105,7 @@ export default mixins(workflowHelpers).extend({
|
||||||
components: {
|
components: {
|
||||||
TagsContainer,
|
TagsContainer,
|
||||||
PushConnectionTracker,
|
PushConnectionTracker,
|
||||||
WorkflowNameShort,
|
ShortenName,
|
||||||
WorkflowActivator,
|
WorkflowActivator,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
TagsDropdown,
|
TagsDropdown,
|
||||||
|
|
|
@ -125,13 +125,16 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
if (this.nodeType !== null && this.nodeType.hasOwnProperty('eventTriggerDescription')) {
|
if (this.nodeType !== null && this.nodeType.hasOwnProperty('eventTriggerDescription')) {
|
||||||
const nodeName = this.$locale.shortNodeType(this.nodeType.name);
|
const nodeName = this.$locale.shortNodeType(this.nodeType.name);
|
||||||
const { eventTriggerDescription } = this.nodeType;
|
const { eventTriggerDescription } = this.nodeType;
|
||||||
return this.$locale.nodeText().eventTriggerDescription(nodeName, eventTriggerDescription);
|
return this.$locale.nodeText().eventTriggerDescription(
|
||||||
|
nodeName,
|
||||||
|
eventTriggerDescription || '',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.$locale.baseText(
|
return this.$locale.baseText(
|
||||||
'node.waitingForYouToCreateAnEventIn',
|
'node.waitingForYouToCreateAnEventIn',
|
||||||
{
|
{
|
||||||
interpolate: {
|
interpolate: {
|
||||||
nodeType: this.nodeType && getTriggerNodeServiceName(this.nodeType.displayName),
|
nodeType: this.nodeType ? getTriggerNodeServiceName(this.nodeType.displayName) : '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue