From 6f9a3f53ae8c98f3013c5e33b2646fd47e9bfdf0 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 27 May 2022 10:56:49 +0300 Subject: [PATCH 001/215] refactor: Aligned n8n-button usage inside of editor-ui. --- .../components/N8nIconButton/IconButton.vue | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index 322b8288d150e..194885365c4e8 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -1,5 +1,9 @@ diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index af3648a261dee..1bd7ebd4f4fc0 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -40,10 +40,10 @@
-
+
- + @@ -57,6 +57,7 @@ +
@@ -894,6 +895,12 @@ export default mixins( .dataContainer { position: relative; height: 100%; + + &:hover { + .actions-group { + opacity: 1; + } + } } .dataDisplay { @@ -975,12 +982,13 @@ export default mixins( } } -.copyButton { - height: 30px; - top: 12px; - right: 24px; +.actions-group { position: absolute; z-index: 10; + top: 12px; + right: var(--spacing-l); + opacity: 0; + transition: opacity 0.3s ease; } .pagination { From c0645e82aba41bbe1faecb9257a2d5d01349d6a9 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 31 May 2022 12:27:54 +0300 Subject: [PATCH 003/215] feat: Extracted code editor into separate form component. --- .../src/components/N8nButton/Button.vue | 7 + .../editor-ui/src/components/CodeEdit.vue | 139 +++++------------- packages/editor-ui/src/components/RunData.vue | 44 +++++- .../src/components/forms/CodeEditor.vue | 101 +++++++++++++ .../editor-ui/src/components/forms/index.ts | 1 + .../src/plugins/i18n/locales/en.json | 3 + 6 files changed, 190 insertions(+), 105 deletions(-) create mode 100644 packages/editor-ui/src/components/forms/CodeEditor.vue create mode 100644 packages/editor-ui/src/components/forms/index.ts diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 60adac4752d5c..e9326af3279dc 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -65,6 +65,13 @@ export default Vue.extend({ }, icon: { type: [String, Array], +<<<<<<< HEAD +======= + }, + round: { + type: Boolean, + default: false, +>>>>>>> 4e3546dbc (feat: Extracted code editor into separate form component.) }, block: { type: Boolean, diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue index b23d734284b84..1b6ece36145e9 100644 --- a/packages/editor-ui/src/components/CodeEdit.vue +++ b/packages/editor-ui/src/components/CodeEdit.vue @@ -8,15 +8,12 @@ :before-close="closeDialog" >
-
+
diff --git a/packages/editor-ui/src/components/forms/index.ts b/packages/editor-ui/src/components/forms/index.ts new file mode 100644 index 0000000000000..345b928ff05d8 --- /dev/null +++ b/packages/editor-ui/src/components/forms/index.ts @@ -0,0 +1 @@ +export { default as CodeEditor } from './CodeEditor.vue'; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 5aaaec4135b31..9911815d55aab 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -632,6 +632,7 @@ "runData.copyParameterPath": "Copy Parameter Path", "runData.copyToClipboard": "Copy to Clipboard", "runData.copyValue": "Copy Value", + "runData.editValue": "Edit Value", "runData.downloadBinaryData": "Download", "runData.executeNode": "Execute Node", "runData.executionTime": "Execution Time", @@ -649,6 +650,8 @@ "runData.showBinaryData": "View", "runData.startTime": "Start Time", "runData.table": "Table", + "runData.editor.save": "Save", + "runData.editor.cancel": "Cancel", "saveButton.save": "@:_reusableBaseText.save", "saveButton.saved": "Saved", "saveButton.saving": "Saving", From 3d4465dabde1cf5ba02f1338988892fcb081afee Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 26 May 2022 10:14:27 +0300 Subject: [PATCH 004/215] feat: Added edit data button on json hover. --- .../design-system/src/components/N8nIconButton/IconButton.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index bbeb7263f8b28..cf1482712d248 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -56,6 +56,10 @@ export default { type: Boolean, default: true, }, + circle: { + type: Boolean, + default: true, + }, }, }; From f1fb2ceef2c06604e9b40c657c6b4a4b731fbd88 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 14 Jun 2022 17:06:43 +0300 Subject: [PATCH 005/215] feat: Added pinData and edit mode methods. --- packages/editor-ui/src/components/RunData.vue | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 6cca80bbe4f02..4c57b9284493a 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -142,8 +142,16 @@
- - + +
Date: Wed, 15 Jun 2022 09:05:50 +0200 Subject: [PATCH 006/215] :fire: Remove conflict markers --- packages/design-system/src/components/N8nButton/Button.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index e9326af3279dc..d82af483819d6 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -65,13 +65,10 @@ export default Vue.extend({ }, icon: { type: [String, Array], -<<<<<<< HEAD -======= }, round: { type: Boolean, default: false, ->>>>>>> 4e3546dbc (feat: Extracted code editor into separate form component.) }, block: { type: Boolean, From 5128ba9e8bd8165e7890a7ea1ff025b8843f414f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 09:06:19 +0200 Subject: [PATCH 007/215] :pencil2: Update i18n keys --- packages/editor-ui/src/plugins/i18n/locales/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 9911815d55aab..da93a26576eeb 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -632,12 +632,13 @@ "runData.copyParameterPath": "Copy Parameter Path", "runData.copyToClipboard": "Copy to Clipboard", "runData.copyValue": "Copy Value", - "runData.editValue": "Edit Value", + "runData.editOutput": "Edit Output", "runData.downloadBinaryData": "Download", "runData.executeNode": "Execute Node", "runData.executionTime": "Execution Time", "runData.fileExtension": "File Extension", "runData.fileName": "File Name", + "runData.invalidPinnedData": "Invalid pinned data", "runData.items": "Items", "runData.json": "JSON", "runData.mimeType": "Mime Type", From 69a0df91371a3f0958e2171f480db502ec5be92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 09:07:02 +0200 Subject: [PATCH 008/215] :zap: Add JSON validation --- packages/editor-ui/src/components/RunData.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 4c57b9284493a..f7e3a535354fc 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -63,7 +63,7 @@ Date: Wed, 1 Jun 2022 15:42:24 +0200 Subject: [PATCH 009/215] :card_file_box: Add `pinData` column to `workflow_entity` --- .../src/databases/entities/WorkflowEntity.ts | 5 +++ .../mysqldb/1654090101303-IntroducePinData.ts | 25 ++++++++++++++ .../src/databases/migrations/mysqldb/index.ts | 2 ++ .../1654090467022-IntroducePinData.ts | 33 +++++++++++++++++++ .../databases/migrations/postgresdb/index.ts | 2 ++ .../sqlite/1654089251344-IntroducePinData.ts | 33 +++++++++++++++++++ .../src/databases/migrations/sqlite/index.ts | 2 ++ 7 files changed, 102 insertions(+) create mode 100644 packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts create mode 100644 packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts create mode 100644 packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index f85e8625abe7b..be15b4b41ec13 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -115,6 +115,11 @@ export class WorkflowEntity implements IWorkflowDb { @OneToMany(() => SharedWorkflow, (sharedWorkflow) => sharedWorkflow.workflow) shared: SharedWorkflow[]; + @Column({ + type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json', + }) + pinData: { [nodeName: string]: unknown }; + @BeforeUpdate() setUpdateDate() { this.updatedAt = new Date(); diff --git a/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts b/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts new file mode 100644 index 0000000000000..54117671f7a16 --- /dev/null +++ b/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers'; +import config from '../../../../config'; + +export class IntroducePinData1654090101303 implements MigrationInterface { + name = 'IntroducePinData1654090101303'; + + async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD \`pinData\` json NOT NULL`, + ); + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN \`pinData\``); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 31993d41cd603..a793ea3828a11 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -15,6 +15,7 @@ import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserMan import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail'; import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; +import { IntroducePinData1654090101303 } from './1654090101303-IntroducePinData'; export const mysqlMigrations = [ InitialMigration1588157391238, @@ -34,4 +35,5 @@ export const mysqlMigrations = [ LowerCaseUserEmail1648740597343, AddUserSettings1652367743993, AddAPIKeyColumn1652905585850, + IntroducePinData1654090101303, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts b/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts new file mode 100644 index 0000000000000..e8ad7ecc1fb41 --- /dev/null +++ b/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers'; +import config from '../../../../config'; + +export class IntroducePinData1654090467022 implements MigrationInterface { + name = 'IntroducePinData1654090467022'; + + async up(queryRunner: QueryRunner) { + logMigrationStart(this.name); + + const schema = config.getEnv('database.postgresdb.schema'); + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query(`SET search_path TO ${schema}`); + + await queryRunner.query( + `ALTER TABLE ${schema}.${tablePrefix}workflow_entity ADD "pinData" json NOT NULL`, + ); + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner) { + const schema = config.getEnv('database.postgresdb.schema'); + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query(`SET search_path TO ${schema}`); + + await queryRunner.query( + `ALTER TABLE ${schema}.${tablePrefix}workflow_entity DROP COLUMN "pinData"`, + ); + } +} diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 30cc17b873cd8..c8e689fb862c9 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -13,6 +13,7 @@ import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserMan import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail'; import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; +import { IntroducePinData1654090467022 } from './1654090467022-IntroducePinData'; export const postgresMigrations = [ InitialMigration1587669153312, @@ -30,4 +31,5 @@ export const postgresMigrations = [ LowerCaseUserEmail1648740597343, AddUserSettings1652367743993, AddAPIKeyColumn1652905585850, + IntroducePinData1654090467022, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts b/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts new file mode 100644 index 0000000000000..0dc1232731d7a --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers'; +import config from '../../../../config'; + +export class IntroducePinData1654089251344 implements MigrationInterface { + name = 'IntroducePinData1654089251344'; + + async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "pinData" text NOT NULL`, + ); + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` RENAME TO "temporary_workflow_entity"`); + await queryRunner.query( + `CREATE TABLE \`${tablePrefix}workflow_entity\` ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text`, + ); + await queryRunner.query( + `INSERT INTO \`${tablePrefix}workflow_entity\` ("id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData") SELECT "id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData" FROM "temporary_workflow_entity"`, + ); + await queryRunner.query(`DROP TABLE "temporary_workflow_entity"`); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 1d51552e91430..c6044c5a7fc41 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -12,6 +12,7 @@ import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserMan import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail'; import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; +import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData'; const sqliteMigrations = [ InitialMigration1588102412422, @@ -28,6 +29,7 @@ const sqliteMigrations = [ LowerCaseUserEmail1648740597343, AddUserSettings1652367743993, AddAPIKeyColumn1652905585850, + IntroducePinData1654089251344, ]; export { sqliteMigrations }; From 80ac532e827f1ac34901d5159f788ad67905fed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 1 Jun 2022 15:51:05 +0200 Subject: [PATCH 010/215] :blue_book: Tighten type --- packages/cli/src/databases/entities/WorkflowEntity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index be15b4b41ec13..74a5090a665c4 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -118,7 +118,7 @@ export class WorkflowEntity implements IWorkflowDb { @Column({ type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json', }) - pinData: { [nodeName: string]: unknown }; + pinData: { [nodeName: string]: object }; @BeforeUpdate() setUpdateDate() { From 9608f2a5c7eb8350c23151ad908948e45834e23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 1 Jun 2022 16:02:22 +0200 Subject: [PATCH 011/215] :zap: Make `pinData` column nullable --- packages/cli/src/databases/entities/WorkflowEntity.ts | 1 + .../migrations/mysqldb/1654090101303-IntroducePinData.ts | 2 +- .../migrations/postgresdb/1654090467022-IntroducePinData.ts | 2 +- .../migrations/sqlite/1654089251344-IntroducePinData.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index 74a5090a665c4..8a2fc2eb6a414 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -117,6 +117,7 @@ export class WorkflowEntity implements IWorkflowDb { @Column({ type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json', + nullable: true, }) pinData: { [nodeName: string]: object }; diff --git a/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts b/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts index 54117671f7a16..c69a0848bea43 100644 --- a/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts +++ b/packages/cli/src/databases/migrations/mysqldb/1654090101303-IntroducePinData.ts @@ -11,7 +11,7 @@ export class IntroducePinData1654090101303 implements MigrationInterface { const tablePrefix = config.getEnv('database.tablePrefix'); await queryRunner.query( - `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD \`pinData\` json NOT NULL`, + `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD \`pinData\` json`, ); logMigrationEnd(this.name); diff --git a/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts b/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts index e8ad7ecc1fb41..6b28194407265 100644 --- a/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts +++ b/packages/cli/src/databases/migrations/postgresdb/1654090467022-IntroducePinData.ts @@ -14,7 +14,7 @@ export class IntroducePinData1654090467022 implements MigrationInterface { await queryRunner.query(`SET search_path TO ${schema}`); await queryRunner.query( - `ALTER TABLE ${schema}.${tablePrefix}workflow_entity ADD "pinData" json NOT NULL`, + `ALTER TABLE ${schema}.${tablePrefix}workflow_entity ADD "pinData" json`, ); logMigrationEnd(this.name); diff --git a/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts b/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts index 0dc1232731d7a..d36bca491baa6 100644 --- a/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts +++ b/packages/cli/src/databases/migrations/sqlite/1654089251344-IntroducePinData.ts @@ -11,7 +11,7 @@ export class IntroducePinData1654089251344 implements MigrationInterface { const tablePrefix = config.getEnv('database.tablePrefix'); await queryRunner.query( - `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "pinData" text NOT NULL`, + `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "pinData" text`, ); logMigrationEnd(this.name); From da692629d373fb83b6dd462b6e707b288b404aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 2 Jun 2022 10:12:50 +0200 Subject: [PATCH 012/215] :zap: Adjust workflow endpoints for pin data --- packages/cli/src/Server.ts | 48 +---- packages/cli/src/api/workflows.api.ts | 170 ++++++++++++++++++ .../cli/test/integration/shared/testDb.ts | 8 +- .../cli/test/integration/shared/types.d.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 17 +- .../test/integration/workflows.api.test.ts | 107 +++++++++++ packages/workflow/src/Interfaces.ts | 1 + 7 files changed, 298 insertions(+), 55 deletions(-) create mode 100644 packages/cli/src/api/workflows.api.ts create mode 100644 packages/cli/test/integration/workflows.api.test.ts diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 822e9f93c2346..613400332dbb7 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -156,9 +156,9 @@ import type { } from './requests'; import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers'; import { ExecutionEntity } from './databases/entities/ExecutionEntity'; -import { SharedWorkflow } from './databases/entities/SharedWorkflow'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants'; import { credentialsController } from './api/credentials.api'; +import { workflowsController } from './api/workflows.api'; import { getInstanceBaseUrl, isEmailSetUp, @@ -946,50 +946,6 @@ class App { }), ); - // Returns a specific workflow - this.app.get( - `/${this.restEndpoint}/workflows/:id`, - ResponseHelper.send(async (req: WorkflowRequest.Get) => { - const { id: workflowId } = req.params; - - let relations = ['workflow', 'workflow.tags']; - - if (config.getEnv('workflowTagsDisabled')) { - relations = relations.filter((relation) => relation !== 'workflow.tags'); - } - - const shared = await Db.collections.SharedWorkflow.findOne({ - relations, - where: whereClause({ - user: req.user, - entityType: 'workflow', - entityId: workflowId, - }), - }); - - if (!shared) { - LoggerProxy.info('User attempted to access a workflow without permissions', { - workflowId, - userId: req.user.id, - }); - throw new ResponseHelper.ResponseError( - `Workflow with ID "${workflowId}" could not be found.`, - undefined, - 404, - ); - } - - const { - workflow: { id, ...rest }, - } = shared; - - return { - id: id.toString(), - ...rest, - }; - }), - ); - // Updates an existing workflow this.app.patch( `/${this.restEndpoint}/workflows/:id`, @@ -1253,6 +1209,8 @@ class App { ), ); + this.app.use(`/${this.restEndpoint}/workflows`, workflowsController); + // Retrieves all tags, with or without usage count this.app.get( `/${this.restEndpoint}/tags`, diff --git a/packages/cli/src/api/workflows.api.ts b/packages/cli/src/api/workflows.api.ts new file mode 100644 index 0000000000000..2d4401a670cc5 --- /dev/null +++ b/packages/cli/src/api/workflows.api.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-param-reassign */ +/* eslint-disable import/no-cycle */ + +import express from 'express'; +import { INode, LoggerProxy } from 'n8n-workflow'; + +import { Db, ResponseHelper, whereClause, WorkflowHelpers } from '..'; +import config from '../../config'; +import * as TagHelpers from '../TagHelpers'; +import { SharedWorkflow } from '../databases/entities/SharedWorkflow'; +import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; +import { validateEntity } from '../GenericHelpers'; +import { InternalHooksManager } from '../InternalHooksManager'; +import { externalHooks } from '../Server'; +import type { WorkflowRequest } from '../requests'; + +export const workflowsController = express.Router(); + +/** + * POST /workflows + */ +workflowsController.post( + '/', + ResponseHelper.send(async (req: WorkflowRequest.Create) => { + delete req.body.id; // delete if sent + + const newWorkflow = new WorkflowEntity(); + + const { nodes = [], ...restOfWorkflow } = req.body; + + const { workflowNodes, workflowPinData } = nodes.reduce<{ + workflowNodes: INode[]; + workflowPinData: { [nodeName: string]: object }; + }>( + (acc, node) => { + const { pinData: nodePinData, ...restOfNode } = node; + if (nodePinData) acc.workflowPinData[node.name] = nodePinData; + acc.workflowNodes.push(restOfNode); + + return acc; + }, + { workflowNodes: [], workflowPinData: {} }, + ); + + Object.assign( + newWorkflow, + { nodes: workflowNodes }, + { pinData: Object.keys(workflowPinData).length > 0 ? JSON.stringify(workflowPinData) : null }, + restOfWorkflow, + ); + + await validateEntity(newWorkflow); + + await externalHooks.run('workflow.create', [newWorkflow]); + + const { tags: tagIds } = req.body; + + if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) { + newWorkflow.tags = await Db.collections.Tag.findByIds(tagIds, { + select: ['id', 'name'], + }); + } + + await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); + + let savedWorkflow: undefined | WorkflowEntity; + + await Db.transaction(async (transactionManager) => { + savedWorkflow = await transactionManager.save(newWorkflow); + + const role = await Db.collections.Role.findOneOrFail({ + name: 'owner', + scope: 'workflow', + }); + + const newSharedWorkflow = new SharedWorkflow(); + + Object.assign(newSharedWorkflow, { + role, + user: req.user, + workflow: savedWorkflow, + }); + + await transactionManager.save(newSharedWorkflow); + }); + + if (!savedWorkflow) { + LoggerProxy.error('Failed to create workflow', { userId: req.user.id }); + throw new ResponseHelper.ResponseError('Failed to save workflow'); + } + + if (tagIds && !config.getEnv('workflowTagsDisabled')) { + savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, { + requestOrder: tagIds, + }); + } + + await externalHooks.run('workflow.afterCreate', [savedWorkflow]); + void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow); + + const { id, ...rest } = savedWorkflow; + + return { + id: id.toString(), + ...rest, + }; + }), +); + +/** + * GET /workflows/:id + */ +workflowsController.get( + '/:id', + ResponseHelper.send(async (req: WorkflowRequest.Get) => { + const { id: workflowId } = req.params; + + let relations = ['workflow', 'workflow.tags']; + + if (config.getEnv('workflowTagsDisabled')) { + relations = relations.filter((relation) => relation !== 'workflow.tags'); + } + + const shared = await Db.collections.SharedWorkflow.findOne({ + relations, + where: whereClause({ + user: req.user, + entityType: 'workflow', + entityId: workflowId, + }), + }); + + if (!shared) { + LoggerProxy.info('User attempted to access a workflow without permissions', { + workflowId, + userId: req.user.id, + }); + throw new ResponseHelper.ResponseError( + `Workflow with ID "${workflowId}" could not be found.`, + undefined, + 404, + ); + } + + const { + workflow: { id, name, nodes, pinData, ...rest }, + } = shared; + + const pinDataObject: { [nodeName: string]: object } | null = + typeof pinData === 'string' ? JSON.parse(pinData) : pinData; + + const workflowNodes = nodes.map((node) => { + if (!pinDataObject) return node; + + if (pinDataObject[node.name]) { + node.pinData = pinDataObject[node.name]; + } + + return node; + }); + + return { + id: id.toString(), + name, + nodes: workflowNodes, + ...rest, + }; + }), +); diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index e3449612353cb..9efd2f3dd3f77 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -140,7 +140,13 @@ export async function truncate(collections: CollectionName[], testDbName: string if (dbType === 'sqlite') { await testDb.query('PRAGMA foreign_keys=OFF'); - await Promise.all(collections.map((collection) => Db.collections[collection].clear())); + await Promise.all( + collections.map((collection) => { + const tableName = toTableName(collection); + Db.collections[collection].clear(); + testDb.query(`DELETE FROM sqlite_sequence WHERE name = '${tableName}';`); // reset autoincrement + }), + ); return testDb.query('PRAGMA foreign_keys=ON'); } diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index c16a44d29f21f..1e20039b6db22 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -17,7 +17,7 @@ export type SmtpTestAccount = { export type ApiPath = 'internal' | 'public'; -type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials' | 'publicApi'; +type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials' | 'workflows' | 'publicApi'; export type CredentialPayload = { name: string; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 723bf11900a1a..40e2f04a4e848 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -55,7 +55,7 @@ import type { TriggerTime, } from './types'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; - +import { workflowsController } from '../../../src/api/workflows.api'; /** * Initialize a test server. @@ -97,16 +97,17 @@ export async function initTestServer({ if (routerEndpoints.length) { const { apiRouters } = await loadPublicApiVersions(testServer.publicApiEndpoint); - const map: Record = { - credentials: credentialsController, - publicApi: apiRouters, + const map: Record = { + credentials: { controller: credentialsController, path: 'credentials' }, + workflows: { controller: workflowsController, path: 'workflows' }, + publicApi: apiRouters }; for (const group of routerEndpoints) { if (group === 'publicApi') { testServer.app.use(...(map[group] as express.Router[])); } else { - testServer.app.use(`/${testServer.restEndpoint}/${group}`, map[group]); + testServer.app.use(`/${testServer.restEndpoint}/${map[group].path}`, map[group].controller); } } } @@ -145,10 +146,10 @@ const classifyEndpointGroups = (endpointGroups: string[]) => { const routerEndpoints: string[] = []; const functionEndpoints: string[] = []; + const ROUTER_GROUP = ['credentials', 'workflows', 'publicApi']; + endpointGroups.forEach((group) => - (group === 'credentials' || group === 'publicApi' ? routerEndpoints : functionEndpoints).push( - group, - ), + (ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group), ); return [routerEndpoints, functionEndpoints]; diff --git a/packages/cli/test/integration/workflows.api.test.ts b/packages/cli/test/integration/workflows.api.test.ts new file mode 100644 index 0000000000000..203c1014f9798 --- /dev/null +++ b/packages/cli/test/integration/workflows.api.test.ts @@ -0,0 +1,107 @@ +import express from 'express'; + +import * as utils from './shared/utils'; +import * as testDb from './shared/testDb'; +import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; +import type { Role } from '../../src/databases/entities/Role'; +import { INode } from 'n8n-workflow'; + +jest.mock('../../src/telemetry'); + +let app: express.Application; +let testDbName = ''; +let globalOwnerRole: Role; + +beforeAll(async () => { + app = utils.initTestServer({ + endpointGroups: ['workflows'], + applyAuth: true, + }); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + + globalOwnerRole = await testDb.getGlobalOwnerRole(); + + utils.initTestLogger(); + utils.initTestTelemetry(); +}); + +beforeEach(async () => { + await testDb.truncate(['User', 'Workflow', 'SharedWorkflow'], testDbName); +}); + +afterAll(async () => { + await testDb.terminate(testDbName); +}); + +test('POST /workflows should store pin data for node in workflow', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + + const workflow = makeWorkflow({ withPinData: true }); + + const response = await authOwnerAgent.post('/workflows').send(workflow); + + expect(response.statusCode).toBe(200); + + const { pinData } = response.body.data; + + expect(JSON.parse(pinData)).toMatchObject({ Spotify: { myKey: 'myValue' } }); +}); + +test('POST /workflows without pin data should store pin data as null', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + + const workflow = makeWorkflow({ withPinData: false }); + + const response = await authOwnerAgent.post('/workflows').send(workflow); + + expect(response.statusCode).toBe(200); + + const { pinData } = response.body.data; + + expect(pinData).toBeNull(); +}); + +test('GET /workflows/:id should retrieve a workflow - node with pin data', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + + const workflow1 = makeWorkflow({ withPinData: true }); + + await authOwnerAgent.post('/workflows').send(workflow1); + + const response1 = await authOwnerAgent.get('/workflows/1'); + + expect(response1.statusCode).toBe(200); + + const { nodes } = response1.body.data as { nodes: INode[] }; + + const node = nodes.find((node) => node.name === 'Spotify'); + + expect(node?.pinData).toMatchObject({ myKey: 'myValue' }); +}); + +function makeWorkflow({ withPinData }: { withPinData: boolean }) { + const workflow = new WorkflowEntity(); + + workflow.name = 'My Workflow'; + workflow.active = false; + workflow.connections = {}; + workflow.nodes = [ + { + name: 'Spotify', + type: 'n8n-nodes-base.spotify', + parameters: { resource: 'track', operation: 'get', id: '123' }, + typeVersion: 1, + position: [740, 240], + }, + ]; + + if (withPinData) { + workflow.nodes[0].pinData = { myKey: 'myValue' }; + } + + return workflow; +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 59b3cff53a69b..ec909b68c1ab3 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -808,6 +808,7 @@ export interface INode { parameters: INodeParameters; credentials?: INodeCredentials; webhookId?: string; + pinData?: object | null; } export interface INodes { From f40ae8a1ebafbec96ad5c42e481305345517e4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 2 Jun 2022 10:36:22 +0200 Subject: [PATCH 013/215] :blue_book: Improve types --- packages/cli/src/api/workflows.api.ts | 4 ++-- packages/cli/src/databases/entities/WorkflowEntity.ts | 2 +- packages/workflow/src/Interfaces.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/api/workflows.api.ts b/packages/cli/src/api/workflows.api.ts index 2d4401a670cc5..0f4a5effee009 100644 --- a/packages/cli/src/api/workflows.api.ts +++ b/packages/cli/src/api/workflows.api.ts @@ -3,7 +3,7 @@ /* eslint-disable import/no-cycle */ import express from 'express'; -import { INode, LoggerProxy } from 'n8n-workflow'; +import { IDataObject, INode, LoggerProxy } from 'n8n-workflow'; import { Db, ResponseHelper, whereClause, WorkflowHelpers } from '..'; import config from '../../config'; @@ -147,7 +147,7 @@ workflowsController.get( workflow: { id, name, nodes, pinData, ...rest }, } = shared; - const pinDataObject: { [nodeName: string]: object } | null = + const pinDataObject: { [nodeName: string]: IDataObject } | null = typeof pinData === 'string' ? JSON.parse(pinData) : pinData; const workflowNodes = nodes.map((node) => { diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index 8a2fc2eb6a414..cf4d9e565fc9a 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -119,7 +119,7 @@ export class WorkflowEntity implements IWorkflowDb { type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json', nullable: true, }) - pinData: { [nodeName: string]: object }; + pinData: { [nodeName: string]: IDataObject }; @BeforeUpdate() setUpdateDate() { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ec909b68c1ab3..a1987c7ccc064 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -808,7 +808,7 @@ export interface INode { parameters: INodeParameters; credentials?: INodeCredentials; webhookId?: string; - pinData?: object | null; + pinData?: IDataObject; } export interface INodes { From a9c8cb43d34ff9b6ef8ccd8760c3457b63ea0148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 2 Jun 2022 10:39:54 +0200 Subject: [PATCH 014/215] :pencil2: Improve wording --- .../cli/test/integration/workflows.api.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/integration/workflows.api.test.ts b/packages/cli/test/integration/workflows.api.test.ts index 203c1014f9798..457a0c5376886 100644 --- a/packages/cli/test/integration/workflows.api.test.ts +++ b/packages/cli/test/integration/workflows.api.test.ts @@ -49,7 +49,7 @@ test('POST /workflows should store pin data for node in workflow', async () => { expect(JSON.parse(pinData)).toMatchObject({ Spotify: { myKey: 'myValue' } }); }); -test('POST /workflows without pin data should store pin data as null', async () => { +test('POST /workflows should set pin data to null if no pin data', async () => { const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); @@ -64,19 +64,19 @@ test('POST /workflows without pin data should store pin data as null', async () expect(pinData).toBeNull(); }); -test('GET /workflows/:id should retrieve a workflow - node with pin data', async () => { +test('GET /workflows/:id should return pin data at node level', async () => { const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - const workflow1 = makeWorkflow({ withPinData: true }); + const workflow = makeWorkflow({ withPinData: true }); - await authOwnerAgent.post('/workflows').send(workflow1); + await authOwnerAgent.post('/workflows').send(workflow); - const response1 = await authOwnerAgent.get('/workflows/1'); + const response = await authOwnerAgent.get('/workflows/1'); - expect(response1.statusCode).toBe(200); + expect(response.statusCode).toBe(200); - const { nodes } = response1.body.data as { nodes: INode[] }; + const { nodes } = response.body.data as { nodes: INode[] }; const node = nodes.find((node) => node.name === 'Spotify'); From ad896101a8c19ec485a78c17498fcbcb1805c9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 14 Jun 2022 17:21:17 +0200 Subject: [PATCH 015/215] Inject pindata into items flow (#3420) * :zap: Inject pin data - Second approach * :fire: Remove unneeded lint exception --- packages/cli/src/Interfaces.ts | 2 + packages/cli/src/Server.ts | 2 + packages/cli/src/WorkflowRunner.ts | 8 +++- packages/cli/src/WorkflowRunnerProcess.ts | 8 +++- packages/cli/src/requests.d.ts | 2 + packages/core/src/WorkflowExecute.ts | 41 ++++++++++++++++--- packages/editor-ui/src/Interface.ts | 2 + .../src/components/mixins/workflowRun.ts | 9 ++++ packages/workflow/src/Interfaces.ts | 5 +++ 9 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index d41381072504c..908b4b61c9270 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -14,6 +14,7 @@ import { ITaskData, ITelemetrySettings, IWorkflowBase as IWorkflowBaseWorkflow, + PinDataPayload, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -645,6 +646,7 @@ export interface IWorkflowExecutionDataProcess { executionMode: WorkflowExecuteMode; executionData?: IRunExecutionData; runData?: IRunData; + pinData?: PinDataPayload; retryOf?: number | string; sessionId?: string; startNodes?: string[]; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 613400332dbb7..112d04a0fd2e8 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1144,6 +1144,7 @@ class App { ): Promise => { const { workflowData } = req.body; const { runData } = req.body; + const { pinData } = req.body; const { startNodes } = req.body; const { destinationNode } = req.body; const executionMode = 'manual'; @@ -1194,6 +1195,7 @@ class App { destinationNode, executionMode, runData, + pinData, sessionId, startNodes, workflowData, diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index cc3b0d7fcabf2..9cce04ffa4063 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -310,7 +310,12 @@ export class WorkflowRunner { // Can execute without webhook so go on const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); - workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); + workflowExecution = workflowExecute.run( + workflow, + undefined, + data.destinationNode, + data.pinData, + ); } else { Logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId }); // Execute only the nodes between start and destination nodes @@ -320,6 +325,7 @@ export class WorkflowRunner { data.runData, data.startNodes, data.destinationNode, + data.pinData, ); } diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index d77f5d329fdec..29146b56ab0f4 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -347,7 +347,12 @@ export class WorkflowRunnerProcess { // Can execute without webhook so go on this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); - return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode); + return this.workflowExecute.run( + this.workflow, + undefined, + this.data.destinationNode, + this.data.pinData, + ); } // Execute only the nodes between start and destination nodes this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); @@ -356,6 +361,7 @@ export class WorkflowRunnerProcess { this.data.runData, this.data.startNodes, this.data.destinationNode, + this.data.pinData, ); } diff --git a/packages/cli/src/requests.d.ts b/packages/cli/src/requests.d.ts index 9499dc25b1f6a..c05adc6fd4e37 100644 --- a/packages/cli/src/requests.d.ts +++ b/packages/cli/src/requests.d.ts @@ -8,6 +8,7 @@ import { INodeCredentialTestRequest, IRunData, IWorkflowSettings, + PinDataPayload, } from 'n8n-workflow'; import { User } from './databases/entities/User'; @@ -71,6 +72,7 @@ export declare namespace WorkflowRequest { { workflowData: IWorkflowDb; runData: IRunData; + pinData: PinDataPayload; startNodes?: string[]; destinationNode?: string; } diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 3f67e11334089..bfd560fb9a62f 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -32,6 +32,7 @@ import { LoggerProxy as Logger, NodeApiError, NodeOperationError, + PinDataPayload, Workflow, WorkflowExecuteMode, WorkflowOperationError, @@ -59,6 +60,7 @@ export class WorkflowExecute { startData: {}, resultData: { runData: {}, + pinData: {}, }, executionData: { contextData: {}, @@ -82,7 +84,12 @@ export class WorkflowExecute { // PCancelable to a regular Promise and does so not allow canceling // active executions anymore // eslint-disable-next-line @typescript-eslint/promise-function-async - run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable { + run( + workflow: Workflow, + startNode?: INode, + destinationNode?: string, + pinData?: PinDataPayload, + ): PCancelable { // Get the nodes to start workflow execution from startNode = startNode || workflow.getStartNode(destinationNode); @@ -121,6 +128,7 @@ export class WorkflowExecute { }, resultData: { runData: {}, + pinData, }, executionData: { contextData: {}, @@ -152,6 +160,7 @@ export class WorkflowExecute { runData: IRunData, startNodes: string[], destinationNode: string, + pinData?: PinDataPayload, // @ts-ignore ): PCancelable { let incomingNodeConnections: INodeConnections | undefined; @@ -258,6 +267,7 @@ export class WorkflowExecute { }, resultData: { runData, + pinData, }, executionData: { contextData: {}, @@ -927,11 +937,32 @@ export class WorkflowExecute { this.mode, ); nodeSuccessData = runNodeData.data; + const { pinData } = this.runExecutionData.resultData; - if (runNodeData.closeFunction) { - // Explanation why we do this can be found in n8n-workflow/Workflow.ts -> runNode - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - closeFunction = runNodeData.closeFunction(); + if (pinData && pinData[executionNode.name] !== undefined) { + const nodePinData = pinData[executionNode.name][runIndex]; + nodeSuccessData = [[{ json: nodePinData }]]; + } else { + Logger.debug(`Running node "${executionNode.name}" started`, { + node: executionNode.name, + workflowId: workflow.id, + }); + const runNodeData = await workflow.runNode( + executionData.node, + executionData.data, + this.runExecutionData, + runIndex, + this.additionalData, + NodeExecuteFunctions, + this.mode, + ); + nodeSuccessData = runNodeData.data; + + if (runNodeData.closeFunction) { + // Explanation why we do this can be found in n8n-workflow/Workflow.ts -> runNode + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + closeFunction = runNodeData.closeFunction(); + } } Logger.debug(`Running node "${executionNode.name}" finished successfully`, { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index dcd983794e097..e6a75070c5e1d 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -21,6 +21,7 @@ import { ITelemetrySettings, IWorkflowSettings as IWorkflowSettingsWorkflow, WorkflowExecuteMode, + PinDataPayload, } from 'n8n-workflow'; export * from 'n8n-design-system/src/types'; @@ -211,6 +212,7 @@ export interface IStartRunData { startNodes?: string[]; destinationNode?: string; runData?: IRunData; + pinData?: PinDataPayload; } export interface IRunDataUi { diff --git a/packages/editor-ui/src/components/mixins/workflowRun.ts b/packages/editor-ui/src/components/mixins/workflowRun.ts index 5bae8fb45856e..f99656f343f31 100644 --- a/packages/editor-ui/src/components/mixins/workflowRun.ts +++ b/packages/editor-ui/src/components/mixins/workflowRun.ts @@ -8,6 +8,7 @@ import { IRunData, IRunExecutionData, NodeHelpers, + PinDataPayload, } from 'n8n-workflow'; import { externalHooks } from '@/components/mixins/externalHooks'; @@ -151,9 +152,16 @@ export const workflowRun = mixins( const workflowData = await this.getWorkflowDataToSave(); + const pinData = Object.values(workflow.nodes).reduce((acc, node) => { + if (node.pinData) acc[node.name] = [node.pinData]; + + return acc; + }, {}); + const startRunData: IStartRunData = { workflowData, runData: newRunData, + pinData, startNodes, }; if (nodeName) { @@ -174,6 +182,7 @@ export const workflowRun = mixins( data: { resultData: { runData: newRunData || {}, + pinData, startNodes, workflowData, }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index a1987c7ccc064..72ba87eb929c6 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -811,6 +811,10 @@ export interface INode { pinData?: IDataObject; } +export interface PinDataPayload { + [nodeName: string]: IDataObject[]; +} + export interface INodes { [key: string]: INode; } @@ -1309,6 +1313,7 @@ export interface IRunExecutionData { resultData: { error?: ExecutionError; runData: IRunData; + pinData?: PinDataPayload; lastNodeExecuted?: string; }; executionData?: { From 52a27573655218e6e1354ef87fe080556ff842cb Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 26 May 2022 10:14:27 +0300 Subject: [PATCH 016/215] feat: Added edit data button on json hover. --- .../design-system/src/components/N8nIconButton/IconButton.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index cf1482712d248..bbeb7263f8b28 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -56,10 +56,6 @@ export default { type: Boolean, default: true, }, - circle: { - type: Boolean, - default: true, - }, }, }; From 65fdfa98c5075c4061162822f2ee66ebab6aef49 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 31 May 2022 12:27:54 +0300 Subject: [PATCH 017/215] feat: Extracted code editor into separate form component. --- packages/design-system/src/components/N8nButton/Button.vue | 7 +++++++ packages/editor-ui/src/plugins/i18n/locales/en.json | 1 + 2 files changed, 8 insertions(+) diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index d82af483819d6..0e710f3700bb7 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -65,6 +65,13 @@ export default Vue.extend({ }, icon: { type: [String, Array], +<<<<<<< HEAD +======= + }, + round: { + type: Boolean, + default: false, +>>>>>>> 4e3546dbc (feat: Extracted code editor into separate form component.) }, round: { type: Boolean, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index da93a26576eeb..0e6b61597eded 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -633,6 +633,7 @@ "runData.copyToClipboard": "Copy to Clipboard", "runData.copyValue": "Copy Value", "runData.editOutput": "Edit Output", + "runData.editValue": "Edit Value", "runData.downloadBinaryData": "Download", "runData.executeNode": "Execute Node", "runData.executionTime": "Execution Time", From 93609dcb2cc4a1c0bcfb3bd05dd3b93aa761811f Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 26 May 2022 10:14:27 +0300 Subject: [PATCH 018/215] feat: Added edit data button on json hover. --- .../design-system/src/components/N8nIconButton/IconButton.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index bbeb7263f8b28..cf1482712d248 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -56,6 +56,10 @@ export default { type: Boolean, default: true, }, + circle: { + type: Boolean, + default: true, + }, }, }; From 5d6c7cba56078db5a96a435a4ad4bb509d1b2241 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 14 Jun 2022 18:35:55 +0300 Subject: [PATCH 019/215] fix: Fixed rebase conflicts. --- packages/design-system/src/components/N8nButton/Button.vue | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 0e710f3700bb7..d82af483819d6 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -65,13 +65,6 @@ export default Vue.extend({ }, icon: { type: [String, Array], -<<<<<<< HEAD -======= - }, - round: { - type: Boolean, - default: false, ->>>>>>> 4e3546dbc (feat: Extracted code editor into separate form component.) }, round: { type: Boolean, From e2c32c00c2650eea99360cfc00acf78d8b707f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 09:14:13 +0200 Subject: [PATCH 020/215] :rewind: Undo button change --- packages/design-system/src/components/N8nButton/Button.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index d82af483819d6..60adac4752d5c 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -66,10 +66,6 @@ export default Vue.extend({ icon: { type: [String, Array], }, - round: { - type: Boolean, - default: false, - }, block: { type: Boolean, default: false, From 7895a832167c54f3f60f88d97e419ad0ec96b6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 09:26:14 +0200 Subject: [PATCH 021/215] :bug: Fix runNode call Adjust per update in bdb84130d687811d65337ff6b025e7cb0eae8256 --- packages/core/src/WorkflowExecute.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index bfd560fb9a62f..cbaef8ed55860 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -924,19 +924,6 @@ export class WorkflowExecute { } } - Logger.debug(`Running node "${executionNode.name}" started`, { - node: executionNode.name, - workflowId: workflow.id, - }); - const runNodeData = await workflow.runNode( - executionData, - this.runExecutionData, - runIndex, - this.additionalData, - NodeExecuteFunctions, - this.mode, - ); - nodeSuccessData = runNodeData.data; const { pinData } = this.runExecutionData.resultData; if (pinData && pinData[executionNode.name] !== undefined) { @@ -948,8 +935,7 @@ export class WorkflowExecute { workflowId: workflow.id, }); const runNodeData = await workflow.runNode( - executionData.node, - executionData.data, + executionData, this.runExecutionData, runIndex, this.additionalData, From d5972913153eaeab5849dd4eb5a8a62cc1700334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 12:36:00 +0200 Subject: [PATCH 022/215] :test_tube: Fix workflow tests --- packages/cli/test/integration/workflows.api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/workflows.api.test.ts b/packages/cli/test/integration/workflows.api.test.ts index 457a0c5376886..d892c44c34f99 100644 --- a/packages/cli/test/integration/workflows.api.test.ts +++ b/packages/cli/test/integration/workflows.api.test.ts @@ -13,7 +13,7 @@ let testDbName = ''; let globalOwnerRole: Role; beforeAll(async () => { - app = utils.initTestServer({ + app = await utils.initTestServer({ endpointGroups: ['workflows'], applyAuth: true, }); From c9bc1190db3b25124359f7e9f45c3c499456ce30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 15 Jun 2022 12:55:11 +0200 Subject: [PATCH 023/215] :bug: More merge conflict fixes --- packages/cli/src/Server.ts | 1 + packages/cli/src/api/workflows.api.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 112d04a0fd2e8..d8d48cee72ca4 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -165,6 +165,7 @@ import { isUserManagementEnabled, } from './UserManagement/UserManagementHelper'; import { loadPublicApiVersions } from './PublicApi'; +import { SharedWorkflow } from './databases/entities/SharedWorkflow'; require('body-parser-xml')(bodyParser); diff --git a/packages/cli/src/api/workflows.api.ts b/packages/cli/src/api/workflows.api.ts index 0f4a5effee009..68319667f06bd 100644 --- a/packages/cli/src/api/workflows.api.ts +++ b/packages/cli/src/api/workflows.api.ts @@ -97,7 +97,7 @@ workflowsController.post( } await externalHooks.run('workflow.afterCreate', [savedWorkflow]); - void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow); + void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false); const { id, ...rest } = savedWorkflow; From 47975ba2b79127739d9c411e27d6684a15135598 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 15 Jun 2022 17:59:22 +0300 Subject: [PATCH 024/215] feat: Added pin/unpin button and store mutations. --- packages/editor-ui/src/Interface.ts | 1 + packages/editor-ui/src/components/RunData.vue | 72 ++++++++++++++++--- .../src/plugins/i18n/locales/en.json | 5 ++ packages/editor-ui/src/plugins/icons.ts | 2 + packages/editor-ui/src/store.ts | 15 ++++ 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e6a75070c5e1d..d015743fb57a2 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -146,6 +146,7 @@ export interface INodeUi extends INode { notes?: string; issues?: INodeIssues; name: string; + pinData?: IDataObject; } export interface INodeTypesMaxCount { diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index f7e3a535354fc..3504c63fc6781 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -11,6 +11,35 @@ :options="buttons" @input="onDisplayModeChange" /> + + + + +
@@ -387,6 +416,9 @@ export default mixins( } return null; }, + hasPinData (): boolean { + return !!(this.node && this.node.pinData); + }, buttons(): Array<{label: string, value: string}> { const defaults = [ { label: this.$locale.baseText('runData.table'), value: 'table'}, @@ -548,15 +580,14 @@ export default mixins( } this.editMode.enabled = false; - this.$store.dispatch('workflow/pinData', { node: this.node, data: this.editMode.value }); + this.$store.commit('pinData', { node: this.node, data: this.editMode.value }); }, - onClickPinData() { - // @TODO - this.$store.dispatch('workflow/pinData', { node: this.node, data: this.editMode.value }); - }, - onClickUnpinData() { - // @TODO - this.$store.commit('workflow/unpinData', { node: this.node }); + onTogglePinData() { + if (this.hasPinData) { + this.$store.commit('unpinData', { node: this.node }); + } else { + this.$store.commit('pinData', { node: this.node, data: this.jsonData }); + } }, switchToBinary() { this.onDisplayModeChange('binary'); @@ -1139,6 +1170,31 @@ export default mixins( flex-grow: 1; } +.pin-data-tooltip { + max-width: 240px; +} + +.pin-data-button { + svg { + transition: transform 0.3s ease; + } +} + +.pin-data-button-active { + &, + &:hover, + &:focus, + &:active { + border-color: var(--color-primary); + color: var(--color-primary); + background: var(--color-primary-tint-2); + } + + svg { + transform: rotate(45deg); + } +} + .spinner { * { color: var(--color-primary); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 0e6b61597eded..97f086d86f5a4 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -354,6 +354,11 @@ "ndv.title.cancel": "Cancel", "ndv.title.rename": "Rename", "ndv.title.renameNode": "Rename node", + "ndv.pinData.pin.title": "Pin data", + "ndv.pinData.pin.description": "Node will always output this data instead of executing.", + "ndv.pinData.pin.link": "How to pin previous output data", + "ndv.pinData.unpin.title": "Output data is pinned.", + "ndv.pinData.unpin.description": "Unpin to execute the node instead.", "noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?", "noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows", "node.activateDeactivateNode": "Activate/Deactivate Node", diff --git a/packages/editor-ui/src/plugins/icons.ts b/packages/editor-ui/src/plugins/icons.ts index c7d46714f1d79..1d70440c66f9d 100644 --- a/packages/editor-ui/src/plugins/icons.ts +++ b/packages/editor-ui/src/plugins/icons.ts @@ -90,6 +90,7 @@ import { faTasks, faTerminal, faThLarge, + faThumbtack, faTimes, faTrash, faUndo, @@ -201,6 +202,7 @@ addIcon(faTerminal); addIcon(faThLarge); addIcon(faTimes); addIcon(faTrash); +addIcon(faThumbtack); addIcon(faUndo); addIcon(faUnlink); addIcon(faUserCircle); diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 26ac7bf79cfdb..931af6b1aa347 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -202,6 +202,21 @@ export const store = new Vuex.Store({ Vue.set(state, 'selectedNodes', []); }, + // Pin data + pinData(state, payload: { node: INodeUi, data: IDataObject }) { + const node = state.workflow.nodes.find((node) => node.name === payload.node.name); + if (node) { + Vue.set(node, 'pinData', payload.data); + } + }, + unpinData(state, payload: { node: INodeUi }) { + const node = state.workflow.nodes.find((node) => node.name === payload.node.name); + if (node) { + Vue.set(node, 'pinData', undefined); + delete node.pinData; + } + }, + // Active setActive (state, newActive: boolean) { state.workflow.active = newActive; From de2a6e4818611352dec83765a741c8357c153520 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 21 Jun 2022 18:21:30 +0300 Subject: [PATCH 025/215] feat: Size check. Various design and ux improvements. --- .../src/components/NodeExecuteButton.vue | 22 ++- .../editor-ui/src/components/OutputPanel.vue | 25 ++- packages/editor-ui/src/components/RunData.vue | 147 ++++++++++++++---- packages/editor-ui/src/components/helpers.ts | 4 + packages/editor-ui/src/constants.ts | 2 + .../src/plugins/i18n/locales/en.json | 13 +- packages/editor-ui/src/store.ts | 24 ++- 7 files changed, 198 insertions(+), 39 deletions(-) diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index 5c84227ed2a8f..d591838abe520 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -64,6 +64,9 @@ export default mixins( workflowRunning (): boolean { return this.$store.getters.isActionActive('workflowRunning'); }, + hasPinData (): boolean { + return this.node !== null && typeof this.node.pinData !== 'undefined'; + }, isTriggerNode (): boolean { return !!(this.nodeType && this.nodeType.group.includes('trigger')); }, @@ -156,11 +159,24 @@ export default mixins( }); }, - onClick() { + async onClick() { if (this.isListeningForEvents) { this.stopWaitingForWebhook(); - } - else { + } else { + if (this.hasPinData) { + const shouldUnpin = await this.confirmMessage( + this.$locale.baseText('ndv.pinData.unpinOnExecute.description'), + this.$locale.baseText('ndv.pinData.unpinOnExecute.title'), + null, + this.$locale.baseText('ndv.pinData.unpinOnExecute.confirm'), + this.$locale.baseText('ndv.pinData.unpinOnExecute.cancel'), + ); + + if (shouldUnpin) { + this.$store.commit('unpinData', { node: this.node }); + } + } + this.$telemetry.track('User clicked execute node button', { node_type: this.nodeName, workflow_id: this.$store.getters.workflowId, source: this.telemetrySource }); this.runWorkflow(this.nodeName, 'RunData.ExecuteNodeButton'); this.$emit('execute'); diff --git a/packages/editor-ui/src/components/OutputPanel.vue b/packages/editor-ui/src/components/OutputPanel.vue index d1f0ddb7c64c7..85a1b485bbdbb 100644 --- a/packages/editor-ui/src/components/OutputPanel.vue +++ b/packages/editor-ui/src/components/OutputPanel.vue @@ -13,6 +13,7 @@ @runChange="onRunIndexChange" @linkRun="onLinkRun" @unlinkRun="onUnlinkRun" + ref="runData" >