From 20ee64a89c1ad4ae74ad0116b106dfe30b29c970 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 13:51:03 +0200 Subject: [PATCH 01/22] feat: Add onboarding flow --- packages/editor-ui/src/constants.ts | 1 + .../editor-ui/src/mixins/workflowHelpers.ts | 27 ++--------- packages/editor-ui/src/router.ts | 23 +++++++++- .../editor-ui/src/stores/templates.store.ts | 18 ++++++++ .../editor-ui/src/stores/workflows.store.ts | 31 ++++++++++++- .../src/views/WorkflowOnboardingView.vue | 46 +++++++++++++++++++ 6 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 packages/editor-ui/src/views/WorkflowOnboardingView.vue diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 443bb434765da..bce08c9364eea 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -361,6 +361,7 @@ export const enum VIEWS { WORKFLOW = 'NodeViewExisting', DEMO = 'WorkflowDemo', TEMPLATE_IMPORT = 'WorkflowTemplate', + WORKFLOW_ONBOARDING = 'WorkflowOnboarding', SIGNIN = 'SigninView', SIGNUP = 'SignupView', SIGNOUT = 'SignoutView', diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index 6b580c2475d33..6efa0f5a417af 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -51,7 +51,6 @@ import { useToast, useMessage } from '@/composables'; import { isEqual } from 'lodash-es'; -import { v4 as uuid } from 'uuid'; import { getSourceItems } from '@/utils'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; @@ -878,27 +877,6 @@ export const workflowHelpers = defineComponent({ const workflowDataRequest: IWorkflowDataUpdate = data || (await this.getWorkflowDataToSave()); - // make sure that the new ones are not active - workflowDataRequest.active = false; - const changedNodes = {} as IDataObject; - - if (resetNodeIds) { - workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => { - node.id = uuid(); - - return node; - }); - } - - if (resetWebhookUrls) { - workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => { - if (node.webhookId) { - node.webhookId = uuid(); - changedNodes[node.name] = node.webhookId; - } - return node; - }); - } if (name) { workflowDataRequest.name = name.trim(); @@ -907,7 +885,10 @@ export const workflowHelpers = defineComponent({ if (tags) { workflowDataRequest.tags = tags; } - const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest); + const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest, { + resetWebhookUrls, + resetNodeIds, + }); this.workflowsStore.addWorkflow(workflowData); diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index d29b8582493b5..600bcce89ec3a 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -41,6 +41,7 @@ import SettingsSourceControl from './views/SettingsSourceControl.vue'; import SettingsExternalSecrets from './views/SettingsExternalSecrets.vue'; import SettingsAuditLogs from './views/SettingsAuditLogs.vue'; import WorkflowHistory from '@/views/WorkflowHistory.vue'; +import WorkflowOnboardingView from '@/views/WorkflowOnboardingView.vue'; import { EnterpriseEditionFeature, VIEWS } from '@/constants'; interface IRouteConfig { @@ -57,11 +58,11 @@ interface IRouteConfig { }; } -function getTemplatesRedirect() { +function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]) { const settingsStore = useSettingsStore(); const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled; if (!isTemplatesEnabled) { - return { name: VIEWS.NOT_FOUND }; + return { name: defaultRedirect || VIEWS.NOT_FOUND }; } return false; @@ -334,6 +335,24 @@ export const routes = [ }, }, }, + { + path: '/workflows/onboarding/:id', + name: VIEWS.WORKFLOW_ONBOARDING, + components: { + default: WorkflowOnboardingView, + header: MainHeader, + sidebar: MainSidebar, + }, + meta: { + templatesEnabled: true, + getRedirect: () => getTemplatesRedirect(VIEWS.NEW_WORKFLOW), + permissions: { + allow: { + loginStatus: [LOGIN_STATUS.LoggedIn], + }, + }, + }, + }, { path: '/workflow/new', name: VIEWS.NEW_WORKFLOW, diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 6d40d7a98ebf8..68d96a306bf0c 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia'; import { STORES } from '@/constants'; import type { + INodeUi, ITemplatesCategory, ITemplatesCollection, ITemplatesCollectionFull, @@ -19,6 +20,7 @@ import { getWorkflows, getWorkflowTemplate, } from '@/api/templates'; +import { getFixedNodesList } from '@/utils/nodeViewUtils'; const TEMPLATES_PAGE_SIZE = 10; @@ -332,5 +334,21 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { const versionCli: string = settingsStore.versionCli; return getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli }); }, + + async getFixedWorkflowTemplate(templateId: string): Promise { + const template = await this.getWorkflowTemplate(templateId); + if (!template) { + throw new Error(`The template with the ID "${templateId}" does not exist.`); + } + + template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; + template.workflow.nodes?.forEach((node) => { + if (node.credentials) { + delete node.credentials; + } + }); + + return template; + }, }, }); diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index ae35bf88c5840..c3822ae81d7f0 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -86,6 +86,7 @@ import { useNDVStore } from './ndv.store'; import { useNodeTypesStore } from './nodeTypes.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; +import { v4 as uuid } from 'uuid'; const defaults: Omit & { settings: NonNullable } = { name: '', @@ -1271,7 +1272,35 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, // Creates a new workflow - async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise { + async createNewWorkflow( + sendData: IWorkflowDataUpdate, + { + resetNodeIds, + resetWebhookUrls, + }: { resetNodeIds?: boolean; resetWebhookUrls?: boolean } = {}, + ): Promise { + const workflowDataRequest = deepCopy(sendData); + // make sure that the new ones are not active + sendData.active = false; + const changedNodes = {} as IDataObject; + + if (resetNodeIds) { + sendData.nodes = (sendData.nodes || []).map((node) => { + node.id = uuid(); + + return node; + }); + } + + if (resetWebhookUrls) { + sendData.nodes = (workflowDataRequest.nodes || []).map((node) => { + if (node.webhookId) { + node.webhookId = uuid(); + changedNodes[node.name] = node.webhookId; + } + return node; + }); + } const rootStore = useRootStore(); return makeRestApiRequest( rootStore.getRestApiContext, diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue new file mode 100644 index 0000000000000..9b7d0bfbe652c --- /dev/null +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -0,0 +1,46 @@ + + + + + From af119c8ac07a96465fadd3cd0558128895858549 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 13:53:47 +0200 Subject: [PATCH 02/22] fix: use updated workflow --- packages/editor-ui/src/stores/workflows.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index c3822ae81d7f0..1b04046d5255d 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -1306,7 +1306,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { rootStore.getRestApiContext, 'POST', '/workflows', - sendData as unknown as IDataObject, + workflowDataRequest as unknown as IDataObject, ); }, From 459744f80bea6b3299cbd6e1831061f3f3e48a71 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 14:22:15 +0200 Subject: [PATCH 03/22] fix: use updated workflow --- packages/editor-ui/src/stores/workflows.store.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 1b04046d5255d..a29489a4e4753 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -1281,11 +1281,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { ): Promise { const workflowDataRequest = deepCopy(sendData); // make sure that the new ones are not active - sendData.active = false; + workflowDataRequest.active = false; const changedNodes = {} as IDataObject; if (resetNodeIds) { - sendData.nodes = (sendData.nodes || []).map((node) => { + workflowDataRequest.nodes = (workflowDataRequest.nodes || []).map((node) => { node.id = uuid(); return node; @@ -1293,7 +1293,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { } if (resetWebhookUrls) { - sendData.nodes = (workflowDataRequest.nodes || []).map((node) => { + workflowDataRequest.nodes = (workflowDataRequest.nodes || []).map((node) => { if (node.webhookId) { node.webhookId = uuid(); changedNodes[node.name] = node.webhookId; From d205af530f78e9be56852aab9973a9a1caa68b4d Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 15:26:11 +0200 Subject: [PATCH 04/22] fix: add workflow metadata --- .../cli/src/databases/entities/WorkflowEntity.ts | 7 +++++++ .../mysqldb/1695128658538-AddWorkflowMetadata.ts | 13 +++++++++++++ .../cli/src/databases/migrations/mysqldb/index.ts | 2 ++ .../postgresdb/1695128658538-AddWorkflowMetadata.ts | 11 +++++++++++ .../src/databases/migrations/postgresdb/index.ts | 2 ++ .../sqlite/1695128658538-AddWorkflowMetadata.ts | 11 +++++++++++ .../cli/src/databases/migrations/sqlite/index.ts | 2 ++ packages/cli/src/requests.ts | 1 + packages/editor-ui/src/Interface.ts | 6 ++++++ packages/editor-ui/src/views/NodeView.vue | 9 +++++++++ .../editor-ui/src/views/WorkflowOnboardingView.vue | 3 +++ 11 files changed, 67 insertions(+) create mode 100644 packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts create mode 100644 packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts create mode 100644 packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index 0bbb70ccf2417..805a7a0a4528c 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -46,6 +46,13 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl }) staticData?: IDataObject; + @Column({ + type: jsonColumnType, + nullable: true, + transformer: objectRetriever, + }) + meta?: IDataObject; + @ManyToMany('TagEntity', 'workflows') @JoinTable({ name: 'workflows_tags', // table name for the junction table of this relation diff --git a/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts new file mode 100644 index 0000000000000..1ba8ebd81cb34 --- /dev/null +++ b/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts @@ -0,0 +1,13 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { + async up({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN \`meta\` json`, + ); + } + + async down({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN \`meta\``); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index b6255dc5f1b59..473044a87b974 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -48,6 +48,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from './1695128658538-AddWorkflowMetadata'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -99,4 +100,5 @@ export const mysqlMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts new file mode 100644 index 0000000000000..8f59021e09638 --- /dev/null +++ b/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts @@ -0,0 +1,11 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { + async up({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ADD COLUMN meta json`); + } + + async down({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity DROP COLUMN meta`); + } +} diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index aa4906c6b2d6b..85eabcce59711 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -46,6 +46,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from '../sqlite/1695128658538-AddWorkflowMetadata'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -95,4 +96,5 @@ export const postgresMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts new file mode 100644 index 0000000000000..55b3c87031f92 --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts @@ -0,0 +1,11 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { + async up({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "meta" text`); + } + + async down({ queryRunner, tablePrefix }: MigrationContext) { + await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN "meta"`); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index d4c4e736b9362..3c91e9d0e3b1c 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -45,6 +45,7 @@ import { AddMfaColumns1690000000030 } from './1690000000040-AddMfaColumns'; import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable'; import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions'; import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftDelete'; +import { AddWorkflowMetadata1695128658538 } from './1695128658538-AddWorkflowMetadata'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -93,6 +94,7 @@ const sqliteMigrations: Migration[] = [ CreateWorkflowHistoryTable1692967111175, DisallowOrphanExecutions1693554410387, ExecutionSoftDelete1693491613982, + AddWorkflowMetadata1695128658538, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 82f2c6a7a1860..4ce69bc81c94f 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -85,6 +85,7 @@ export declare namespace WorkflowRequest { active: boolean; tags: string[]; hash: string; + meta: Record; }>; type ManualRunPayload = { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 7988eaa838947..8b319a4730f8a 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -213,6 +213,7 @@ export interface IWorkflowDataUpdate { tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response pinData?: IPinData; versionId?: string; + meta?: WorkflowMetadata; } export interface IWorkflowToShare extends IWorkflowDataUpdate { @@ -235,6 +236,10 @@ export interface INewWorkflowData { onboardingFlowEnabled: boolean; } +export interface WorkflowMetadata { + onboardingId?: string; +} + // Almost identical to cli.Interfaces.ts export interface IWorkflowDb { id: string; @@ -251,6 +256,7 @@ export interface IWorkflowDb { ownedBy?: Partial; versionId: string; usedCredentials?: IUsedCredential[]; + meta?: WorkflowMetadata; } // Identical to cli.Interfaces.ts diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 8a9b3d9d743ef..5d6b71d8fe676 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2635,6 +2635,15 @@ export default defineComponent({ this.titleSet(workflow.name, 'IDLE'); await this.openWorkflow(workflow); await this.checkAndInitDebugMode(); + + if (workflow.meta?.onboardingId) { + this.$telemetry.track( + `User opened workflow from ${workflow.meta.onboardingId} onboarding template`, + { + workflow_id: workflow.id, + }, + ); + } } } else if (this.$route.meta?.nodeView === true) { // Create new workflow diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue index 9b7d0bfbe652c..c1c53fc9c0c49 100644 --- a/packages/editor-ui/src/views/WorkflowOnboardingView.vue +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -16,6 +16,9 @@ const openWorkflowTemplate = async (templateId: string) => { name: template.name, connections: template.workflow.connections, nodes: template.workflow.nodes, + meta: { + onboardingId: templateId, + }, }); await router.replace({ From c4fc430a591c8ba56b71f647557820c1e1e8ff9f Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 18:22:08 +0200 Subject: [PATCH 05/22] test: add workflow store tests --- .../src/stores/__tests__/workflows.spec.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/editor-ui/src/stores/__tests__/workflows.spec.ts diff --git a/packages/editor-ui/src/stores/__tests__/workflows.spec.ts b/packages/editor-ui/src/stores/__tests__/workflows.spec.ts new file mode 100644 index 0000000000000..772cecec4c4da --- /dev/null +++ b/packages/editor-ui/src/stores/__tests__/workflows.spec.ts @@ -0,0 +1,120 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { IWorkflowDataUpdate } from '@/Interface'; +import { makeRestApiRequest } from '@/utils'; +import { useRootStore } from '../n8nRoot.store'; + +vi.mock('@/utils', () => ({ + makeRestApiRequest: vi.fn(), +})); + +const MOCK_WORKFLOW_SIMPLE: IWorkflowDataUpdate = { + id: '1', + name: 'test', + nodes: [ + { + parameters: { + path: '21a77783-e050-4e0f-9915-2d2dd5b53cde', + options: {}, + }, + id: '2dbf9369-2eec-42e7-9b89-37e50af12289', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + typeVersion: 1, + position: [340, 240], + webhookId: '21a77783-e050-4e0f-9915-2d2dd5b53cde', + }, + { + parameters: { + table: 'product', + columns: 'name,ean', + additionalFields: {}, + }, + name: 'Insert Rows1', + type: 'n8n-nodes-base.postgres', + position: [580, 240], + typeVersion: 1, + id: 'a10ba62a-8792-437c-87df-0762fa53e157', + credentials: { + postgres: { + id: 'iEFl08xIegmR8xF6', + name: 'Postgres account', + }, + }, + }, + ], + connections: { + Webhook: { + main: [ + [ + { + node: 'Insert Rows1', + type: 'main', + index: 0, + }, + ], + ], + }, + }, +}; + +describe('worklfows store', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + describe('createNewWorkflow', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('creates new workflow', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE); + + expect(makeRestApiRequest).toHaveBeenCalledWith( + useRootStore().getRestApiContext, + 'POST', + '/workflows', + { + ...MOCK_WORKFLOW_SIMPLE, + active: false, + }, + ); + }); + + it('sets active to false', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow({ ...MOCK_WORKFLOW_SIMPLE, active: true }); + + expect(makeRestApiRequest).toHaveBeenCalledWith( + useRootStore().getRestApiContext, + 'POST', + '/workflows', + { + ...MOCK_WORKFLOW_SIMPLE, + active: false, + }, + ); + }); + + it('resets node ids', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE, { resetNodeIds: true }); + + const workflow = makeRestApiRequest.mock.calls[0][3]; + + expect(workflow.nodes[0].id).not.toBe(MOCK_WORKFLOW_SIMPLE.nodes![0].id); + expect(workflow.nodes[1].id).not.toBe(MOCK_WORKFLOW_SIMPLE.nodes![1].id); + }); + + it('resets webhook urls', async () => { + const workflowsStore = useWorkflowsStore(); + await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE, { resetWebhookUrls: true }); + + const workflow = makeRestApiRequest.mock.calls[0][3]; + + expect(workflow.nodes[0].webhookId).not.toBe(MOCK_WORKFLOW_SIMPLE.nodes![0].webhookId); + }); + }); +}); From 3d435dda7dec8706ca30861594bd3d444822e845 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 19:04:15 +0200 Subject: [PATCH 06/22] test: add e2e tests --- cypress/e2e/29-templates.cy.ts | 21 ++++++++++++++ cypress/pages/templates.ts | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 cypress/e2e/29-templates.cy.ts create mode 100644 cypress/pages/templates.ts diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts new file mode 100644 index 0000000000000..c44bf4feb8f0d --- /dev/null +++ b/cypress/e2e/29-templates.cy.ts @@ -0,0 +1,21 @@ +import { TemplatesPage } from '../pages/templates'; +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const templatesPage = new TemplatesPage(); +const WorkflowPage = new WorkflowPageClass(); + +describe('Templates', () => { + // it('can open onboarding flow', () => { + // templatesPage.actions.openOnboardingFlow(); + + // WorkflowPage.getters.canvasNodes().should('have.length', 4); + // WorkflowPage.getters.stickies().should('have.length', 2); + // }); + + it('can import template', () => { + templatesPage.actions.openTemplateImportFlow(); + + WorkflowPage.getters.canvasNodes().should('have.length', 4); + WorkflowPage.getters.stickies().should('have.length', 2); + }); +}); diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts new file mode 100644 index 0000000000000..5bf89370c9b6d --- /dev/null +++ b/cypress/pages/templates.ts @@ -0,0 +1,50 @@ +import { BasePage } from './base'; + +const MOCK_WORKFLOW = {"id":1751,"name":"Preparing data to be sent to a service","workflow":{"nodes":[{"name":"On clicking 'execute'","type":"n8n-nodes-base.manualTrigger","position":[1160,480],"parameters":{},"typeVersion":1},{"name":"Note","type":"n8n-nodes-base.stickyNote","position":[800,420],"parameters":{"width":320,"height":200,"content":"### Very often your data is not in the right format to insert in a node. you can use the set node to fix it.\n\n### Click the `Execute Workflow` button and double click on the nodes to see the input and output items."},"typeVersion":1},{"name":"Create or Update record in Google Sheet","type":"n8n-nodes-base.googleSheets","position":[1920,480],"parameters":{"range":"A:C","options":{},"sheetId":"13_bAEYNTzVXVY6SfAkBa9ijtJGSxPd8D-hcXXwXtdDo","operation":"upsert","authentication":"oAuth2"},"credentials":{"googleSheetsOAuth2Api":{"id":"8","name":"Sheets"}},"typeVersion":1},{"name":"Note1","type":"n8n-nodes-base.stickyNote","position":[1480,360],"parameters":{"width":400,"height":280,"content":"\nThis is where we put the data in the format that Google Sheets expect. \nThis means changing the field name from `name` to `Full name`, dropping all fields except `ID`, `Email` and adding a `Created time` field"},"typeVersion":1},{"name":"Set - Prepare fields","type":"n8n-nodes-base.set","notes":"Prepare fields","position":[1620,480],"parameters":{"values":{"number":[{"name":"ID","value":"={{$json[\"id\"]}}"}],"string":[{"name":"Full name","value":"={{$json[\"name\"]}}"},{"name":"Email","value":"={{$json[\"email\"]}}"},{"name":"Created time","value":"={{$now}}"}]},"options":{},"keepOnlySet":true},"notesInFlow":false,"typeVersion":1},{"name":"Customer Datastore - Generate some data","type":"n8n-nodes-base.n8nTrainingCustomerDatastore","position":[1340,480],"parameters":{"operation":"getAllPeople"},"typeVersion":1}],"connections":{"Set - Prepare fields":{"main":[[{"node":"Create or Update record in Google Sheet","type":"main","index":0}]]},"On clicking 'execute'":{"main":[[{"node":"Customer Datastore - Generate some data","type":"main","index":0}]]},"Customer Datastore - Generate some data":{"main":[[{"node":"Set - Prepare fields","type":"main","index":0}]]}}}}; + +export class TemplatesPage extends BasePage { + url = '/templates'; + + getters = { + }; + + actions = { + openOnboardingFlow: () => { + const templateId = 1234; + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); + cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${templateId}`, { + statusCode: 200, + body: MOCK_WORKFLOW, + }).as('getTemplate'); + cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); + + cy.visit(`/workflows/onboarding/${templateId}`); + + cy.wait('@getTemplate'); + cy.wait(['@createWorkflow', '@getWorkflow']); + + cy.url().then(($url) => { + expect($url).to.match(/.*\/workflow\/.*?onboardingId=1234$/); + }) + }, + + openTemplateImportFlow: () => { + const templateId = 1234; + cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${templateId}`, { + statusCode: 200, + body: MOCK_WORKFLOW, + }).as('getTemplate'); + cy.intercept('GET', 'rest/workflows/**').as('getWorkflow'); + + cy.visit(`/workflows/templates/${templateId}`); + + cy.wait('@getTemplate'); + cy.wait( '@getWorkflow'); + + cy.url().then(($url) => { + expect($url).to.include('/workflow/new?templateId=1234'); + }) + } + } +} + From c5b77d0c842b552d324f4f87f4e8ac78ee4f7dc2 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 19:05:52 +0200 Subject: [PATCH 07/22] test: enable --- cypress/e2e/29-templates.cy.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index c44bf4feb8f0d..b108fde7bd2d0 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -5,12 +5,12 @@ const templatesPage = new TemplatesPage(); const WorkflowPage = new WorkflowPageClass(); describe('Templates', () => { - // it('can open onboarding flow', () => { - // templatesPage.actions.openOnboardingFlow(); + it('can open onboarding flow', () => { + templatesPage.actions.openOnboardingFlow(); - // WorkflowPage.getters.canvasNodes().should('have.length', 4); - // WorkflowPage.getters.stickies().should('have.length', 2); - // }); + WorkflowPage.getters.canvasNodes().should('have.length', 4); + WorkflowPage.getters.stickies().should('have.length', 2); + }); it('can import template', () => { templatesPage.actions.openTemplateImportFlow(); From c47fccbc7f5b4b799f47371612689164343a6161 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 19:06:34 +0200 Subject: [PATCH 08/22] fix: tests --- cypress/e2e/29-templates.cy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index b108fde7bd2d0..415d6d01caa39 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -1,21 +1,21 @@ import { TemplatesPage } from '../pages/templates'; -import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; +import { WorkflowPage } from '../pages/workflow'; const templatesPage = new TemplatesPage(); -const WorkflowPage = new WorkflowPageClass(); +const workflowPage = new WorkflowPage(); describe('Templates', () => { it('can open onboarding flow', () => { templatesPage.actions.openOnboardingFlow(); - WorkflowPage.getters.canvasNodes().should('have.length', 4); - WorkflowPage.getters.stickies().should('have.length', 2); + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 2); }); it('can import template', () => { templatesPage.actions.openTemplateImportFlow(); - WorkflowPage.getters.canvasNodes().should('have.length', 4); - WorkflowPage.getters.stickies().should('have.length', 2); + workflowPage.getters.canvasNodes().should('have.length', 4); + workflowPage.getters.stickies().should('have.length', 2); }); }); From 610ab9944bbfab7a4012f23740bf1c89bb3ca289 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Tue, 19 Sep 2023 19:13:10 +0200 Subject: [PATCH 09/22] chore: refactor --- .../editor-ui/src/stores/templates.store.ts | 18 ++++++++---------- packages/editor-ui/src/views/NodeView.vue | 10 +--------- .../src/views/WorkflowOnboardingView.vue | 4 ++++ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 68d96a306bf0c..ab80091fdf8d3 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -335,19 +335,17 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { return getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli }); }, - async getFixedWorkflowTemplate(templateId: string): Promise { + async getFixedWorkflowTemplate(templateId: string): Promise { const template = await this.getWorkflowTemplate(templateId); - if (!template) { - throw new Error(`The template with the ID "${templateId}" does not exist.`); + if (template?.workflow?.nodes) { + template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; + template.workflow.nodes?.forEach((node) => { + if (node.credentials) { + delete node.credentials; + } + }); } - template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; - template.workflow.nodes?.forEach((node) => { - if (node.credentials) { - delete node.credentials; - } - }); - return template; }, }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 5d6b71d8fe676..f2c1f2dbc768c 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -886,7 +886,7 @@ export default defineComponent({ let data: IWorkflowTemplate | undefined; try { void this.$externalHooks().run('template.requested', { templateId }); - data = await this.templatesStore.getWorkflowTemplate(templateId); + data = await this.templatesStore.getFixedWorkflowTemplate(templateId); if (!data) { throw new Error( @@ -901,14 +901,6 @@ export default defineComponent({ return; } - data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes) as INodeUi[]; - - data.workflow.nodes?.forEach((node) => { - if (node.credentials) { - delete node.credentials; - } - }); - this.blankRedirect = true; await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } }); diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue index c1c53fc9c0c49..9dbf2b5e4207f 100644 --- a/packages/editor-ui/src/views/WorkflowOnboardingView.vue +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -12,6 +12,10 @@ const route = useRoute(); const openWorkflowTemplate = async (templateId: string) => { try { const template = await templateStore.getFixedWorkflowTemplate(templateId); + if (!template) { + throw new Error(); + } + const workflow = await workfowStore.createNewWorkflow({ name: template.name, connections: template.workflow.connections, From d68a1ec13782fc068d407ee656eeeb2ed1b6bae8 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Wed, 20 Sep 2023 15:25:40 +0200 Subject: [PATCH 10/22] fix: add loading --- packages/editor-ui/src/views/WorkflowOnboardingView.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue index 9dbf2b5e4207f..3e0db8ac217a9 100644 --- a/packages/editor-ui/src/views/WorkflowOnboardingView.vue +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -1,9 +1,11 @@ From d1ce7c75ce58cb5303864fabfc95d06540dbf619 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Wed, 20 Sep 2023 15:30:32 +0200 Subject: [PATCH 11/22] fix: migraitons --- .../mysqldb/1695128658538-AddWorkflowMetadata.ts | 10 ++++------ .../postgresdb/1695128658538-AddWorkflowMetadata.ts | 8 ++++---- .../sqlite/1695128658538-AddWorkflowMetadata.ts | 8 ++++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts index 1ba8ebd81cb34..84ab2ba47abed 100644 --- a/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts +++ b/packages/cli/src/databases/migrations/mysqldb/1695128658538-AddWorkflowMetadata.ts @@ -1,13 +1,11 @@ import type { MigrationContext, ReversibleMigration } from '@db/types'; export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { - async up({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query( - `ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN \`meta\` json`, - ); + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { + await addColumns('workflow_entity', [column('meta').json]); } - async down({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN \`meta\``); + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('workflow_entity', ['meta']); } } diff --git a/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts index 8f59021e09638..84ab2ba47abed 100644 --- a/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts +++ b/packages/cli/src/databases/migrations/postgresdb/1695128658538-AddWorkflowMetadata.ts @@ -1,11 +1,11 @@ import type { MigrationContext, ReversibleMigration } from '@db/types'; export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { - async up({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ADD COLUMN meta json`); + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { + await addColumns('workflow_entity', [column('meta').json]); } - async down({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity DROP COLUMN meta`); + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('workflow_entity', ['meta']); } } diff --git a/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts index 55b3c87031f92..84ab2ba47abed 100644 --- a/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts +++ b/packages/cli/src/databases/migrations/sqlite/1695128658538-AddWorkflowMetadata.ts @@ -1,11 +1,11 @@ import type { MigrationContext, ReversibleMigration } from '@db/types'; export class AddWorkflowMetadata1695128658538 implements ReversibleMigration { - async up({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "meta" text`); + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { + await addColumns('workflow_entity', [column('meta').json]); } - async down({ queryRunner, tablePrefix }: MigrationContext) { - await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN "meta"`); + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('workflow_entity', ['meta']); } } From 78ec55b7fda27d5aca3d6381b75f706355f0943b Mon Sep 17 00:00:00 2001 From: Mutasem Date: Wed, 20 Sep 2023 17:41:12 +0200 Subject: [PATCH 12/22] fix: add sentry reporting --- packages/editor-ui/src/views/WorkflowOnboardingView.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue index 3e0db8ac217a9..871e82e9a086a 100644 --- a/packages/editor-ui/src/views/WorkflowOnboardingView.vue +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -13,6 +13,7 @@ const route = useRoute(); const openWorkflowTemplate = async (templateId: string) => { try { + loadingService.startLoading(); const template = await templateStore.getFixedWorkflowTemplate(templateId); if (!template) { throw new Error(); @@ -32,8 +33,13 @@ const openWorkflowTemplate = async (templateId: string) => { params: { name: workflow.id }, query: { onboardingId: templateId }, }); + + loadingService.stopLoading(); } catch (e) { await router.replace({ name: VIEWS.NEW_WORKFLOW }); + loadingService.stopLoading(); + + throw new Error(`Could not load onboarding template ${templateId}`); // sentry reporing } }; @@ -44,9 +50,7 @@ onMounted(async () => { return; } - loadingService.startLoading(); await openWorkflowTemplate(templateId); - loadingService.stopLoading(); }); From 3c28e79922e9f63dc5032d5cf53890f2e01e82f9 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Thu, 21 Sep 2023 11:43:05 +0200 Subject: [PATCH 13/22] fix: address product feedback --- packages/editor-ui/src/plugins/i18n/locales/en.json | 1 + packages/editor-ui/src/views/NodeView.vue | 2 +- packages/editor-ui/src/views/WorkflowOnboardingView.vue | 9 +++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ff43e76679106..cfc9541587700 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1553,6 +1553,7 @@ "tagsTableHeader.searchTags": "Search Tags", "tagsView.inUse": "{count} workflow | {count} workflows", "tagsView.notBeingUsed": "Not being used", + "onboarding.title": "Demo: {name}", "template.buttons.goBackButton": "Go back", "template.buttons.useThisWorkflowButton": "Use this workflow", "template.details.appsInTheCollection": "This collection features", diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f2c1f2dbc768c..62a08af09a475 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2630,7 +2630,7 @@ export default defineComponent({ if (workflow.meta?.onboardingId) { this.$telemetry.track( - `User opened workflow from ${workflow.meta.onboardingId} onboarding template`, + `User opened workflow from onboarding template with ID ${workflow.meta.onboardingId}`, { workflow_id: workflow.id, }, diff --git a/packages/editor-ui/src/views/WorkflowOnboardingView.vue b/packages/editor-ui/src/views/WorkflowOnboardingView.vue index 871e82e9a086a..235598bb87d95 100644 --- a/packages/editor-ui/src/views/WorkflowOnboardingView.vue +++ b/packages/editor-ui/src/views/WorkflowOnboardingView.vue @@ -1,5 +1,5 @@