mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: Add onboarding flow (#7212)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
parent
60c152dc72
commit
01e9340621
34
cypress/e2e/29-templates.cy.ts
Normal file
34
cypress/e2e/29-templates.cy.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { TemplatesPage } from '../pages/templates';
|
||||||
|
import { WorkflowPage } from '../pages/workflow';
|
||||||
|
|
||||||
|
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
|
||||||
|
|
||||||
|
const templatesPage = new TemplatesPage();
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
|
describe('Templates', () => {
|
||||||
|
it('can open onboarding flow', () => {
|
||||||
|
templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow);
|
||||||
|
cy.url().then(($url) => {
|
||||||
|
expect($url).to.match(/.*\/workflow\/.*?onboardingId=1234$/);
|
||||||
|
})
|
||||||
|
|
||||||
|
workflowPage.actions.shouldHaveWorkflowName(`Demo: ${name}`);
|
||||||
|
|
||||||
|
workflowPage.getters.canvasNodes().should('have.length', 4);
|
||||||
|
workflowPage.getters.stickies().should('have.length', 1);
|
||||||
|
workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can import template', () => {
|
||||||
|
templatesPage.actions.importTemplate(1234, OnboardingWorkflow.name, OnboardingWorkflow);
|
||||||
|
|
||||||
|
cy.url().then(($url) => {
|
||||||
|
expect($url).to.include('/workflow/new?templateId=1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowPage.getters.canvasNodes().should('have.length', 4);
|
||||||
|
workflowPage.getters.stickies().should('have.length', 1);
|
||||||
|
workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name);
|
||||||
|
});
|
||||||
|
});
|
1020
cypress/fixtures/Onboarding_workflow.json
Normal file
1020
cypress/fixtures/Onboarding_workflow.json
Normal file
File diff suppressed because it is too large
Load diff
50
cypress/pages/templates.ts
Normal file
50
cypress/pages/templates.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { BasePage } from './base';
|
||||||
|
import { WorkflowPage } from './workflow';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
export class TemplatesPage extends BasePage {
|
||||||
|
url = '/templates';
|
||||||
|
|
||||||
|
getters = {
|
||||||
|
};
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
openOnboardingFlow: (id: number, name: string , workflow: object) => {
|
||||||
|
const apiResponse = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
workflow,
|
||||||
|
};
|
||||||
|
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
||||||
|
cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, {
|
||||||
|
statusCode: 200,
|
||||||
|
body: apiResponse,
|
||||||
|
}).as('getTemplate');
|
||||||
|
cy.intercept('GET', 'rest/workflows/**').as('getWorkflow');
|
||||||
|
|
||||||
|
cy.visit(`/workflows/onboarding/${id}`);
|
||||||
|
|
||||||
|
cy.wait('@getTemplate');
|
||||||
|
cy.wait(['@createWorkflow', '@getWorkflow']);
|
||||||
|
},
|
||||||
|
|
||||||
|
importTemplate: (id: number, name: string, workflow: object) => {
|
||||||
|
const apiResponse = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
workflow,
|
||||||
|
};
|
||||||
|
cy.intercept('GET', `https://api.n8n.io/api/workflows/templates/${id}`, {
|
||||||
|
statusCode: 200,
|
||||||
|
body: apiResponse,
|
||||||
|
}).as('getTemplate');
|
||||||
|
cy.intercept('GET', 'rest/workflows/**').as('getWorkflow');
|
||||||
|
|
||||||
|
cy.visit(`/workflows/templates/${id}`);
|
||||||
|
|
||||||
|
cy.wait('@getTemplate');
|
||||||
|
cy.wait( '@getWorkflow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -304,5 +304,11 @@ export class WorkflowPage extends BasePage {
|
||||||
editSticky: (content: string) => {
|
editSticky: (content: string) => {
|
||||||
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
||||||
},
|
},
|
||||||
|
shouldHaveWorkflowName: (name: string) => {
|
||||||
|
this.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('include', name);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Length } from 'class-validator';
|
import { Length } from 'class-validator';
|
||||||
|
|
||||||
import { IConnections, IDataObject, IWorkflowSettings } from 'n8n-workflow';
|
import { IConnections, IDataObject, IWorkflowSettings, WorkflowFEMeta } from 'n8n-workflow';
|
||||||
import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow';
|
import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow';
|
||||||
|
|
||||||
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from 'typeorm';
|
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from 'typeorm';
|
||||||
|
@ -46,6 +46,13 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
|
||||||
})
|
})
|
||||||
staticData?: IDataObject;
|
staticData?: IDataObject;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: jsonColumnType,
|
||||||
|
nullable: true,
|
||||||
|
transformer: objectRetriever,
|
||||||
|
})
|
||||||
|
meta?: WorkflowFEMeta;
|
||||||
|
|
||||||
@ManyToMany('TagEntity', 'workflows')
|
@ManyToMany('TagEntity', 'workflows')
|
||||||
@JoinTable({
|
@JoinTable({
|
||||||
name: 'workflows_tags', // table name for the junction table of this relation
|
name: 'workflows_tags', // table name for the junction table of this relation
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { MigrationContext, ReversibleMigration } from '@db/types';
|
||||||
|
|
||||||
|
export class AddWorkflowMetadata1695128658538 implements ReversibleMigration {
|
||||||
|
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
|
||||||
|
await addColumns('workflow_entity', [column('meta').json]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
|
||||||
|
await dropColumns('workflow_entity', ['meta']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu
|
||||||
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
||||||
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
||||||
import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete';
|
import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete';
|
||||||
|
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
|
||||||
|
|
||||||
export const mysqlMigrations: Migration[] = [
|
export const mysqlMigrations: Migration[] = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -99,4 +100,5 @@ export const mysqlMigrations: Migration[] = [
|
||||||
CreateWorkflowHistoryTable1692967111175,
|
CreateWorkflowHistoryTable1692967111175,
|
||||||
DisallowOrphanExecutions1693554410387,
|
DisallowOrphanExecutions1693554410387,
|
||||||
ExecutionSoftDelete1693491613982,
|
ExecutionSoftDelete1693491613982,
|
||||||
|
AddWorkflowMetadata1695128658538,
|
||||||
];
|
];
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColu
|
||||||
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
||||||
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
||||||
import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete';
|
import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete';
|
||||||
|
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
|
||||||
|
|
||||||
export const postgresMigrations: Migration[] = [
|
export const postgresMigrations: Migration[] = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -95,4 +96,5 @@ export const postgresMigrations: Migration[] = [
|
||||||
CreateWorkflowHistoryTable1692967111175,
|
CreateWorkflowHistoryTable1692967111175,
|
||||||
DisallowOrphanExecutions1693554410387,
|
DisallowOrphanExecutions1693554410387,
|
||||||
ExecutionSoftDelete1693491613982,
|
ExecutionSoftDelete1693491613982,
|
||||||
|
AddWorkflowMetadata1695128658538,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AddWorkflowMetadata1695128658538 as BaseMigration } from '../common/1695128658538-AddWorkflowMetadata';
|
||||||
|
|
||||||
|
export class AddWorkflowMetadata1695128658538 extends BaseMigration {
|
||||||
|
transaction = false as const;
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ import { AddMfaColumns1690000000030 } from './1690000000040-AddMfaColumns';
|
||||||
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
|
||||||
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
import { DisallowOrphanExecutions1693554410387 } from '../common/1693554410387-DisallowOrphanExecutions';
|
||||||
import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftDelete';
|
import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftDelete';
|
||||||
|
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
|
||||||
|
|
||||||
const sqliteMigrations: Migration[] = [
|
const sqliteMigrations: Migration[] = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -93,6 +94,7 @@ const sqliteMigrations: Migration[] = [
|
||||||
CreateWorkflowHistoryTable1692967111175,
|
CreateWorkflowHistoryTable1692967111175,
|
||||||
DisallowOrphanExecutions1693554410387,
|
DisallowOrphanExecutions1693554410387,
|
||||||
ExecutionSoftDelete1693491613982,
|
ExecutionSoftDelete1693491613982,
|
||||||
|
AddWorkflowMetadata1695128658538,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
|
@ -85,6 +85,7 @@ export declare namespace WorkflowRequest {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
hash: string;
|
hash: string;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ManualRunPayload = {
|
type ManualRunPayload = {
|
||||||
|
|
|
@ -214,6 +214,7 @@ export interface IWorkflowDataUpdate {
|
||||||
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
||||||
pinData?: IPinData;
|
pinData?: IPinData;
|
||||||
versionId?: string;
|
versionId?: string;
|
||||||
|
meta?: WorkflowMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
||||||
|
@ -225,10 +226,7 @@ export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
||||||
export interface IWorkflowTemplate {
|
export interface IWorkflowTemplate {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
workflow: {
|
workflow: Pick<IWorkflowData, 'nodes' | 'connections' | 'settings' | 'pinData'>;
|
||||||
nodes: INodeUi[];
|
|
||||||
connections: IConnections;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INewWorkflowData {
|
export interface INewWorkflowData {
|
||||||
|
@ -236,6 +234,10 @@ export interface INewWorkflowData {
|
||||||
onboardingFlowEnabled: boolean;
|
onboardingFlowEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowMetadata {
|
||||||
|
onboardingId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Almost identical to cli.Interfaces.ts
|
// Almost identical to cli.Interfaces.ts
|
||||||
export interface IWorkflowDb {
|
export interface IWorkflowDb {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -252,6 +254,7 @@ export interface IWorkflowDb {
|
||||||
ownedBy?: Partial<IUser>;
|
ownedBy?: Partial<IUser>;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
usedCredentials?: IUsedCredential[];
|
usedCredentials?: IUsedCredential[];
|
||||||
|
meta?: WorkflowMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identical to cli.Interfaces.ts
|
// Identical to cli.Interfaces.ts
|
||||||
|
|
|
@ -361,6 +361,7 @@ export const enum VIEWS {
|
||||||
WORKFLOW = 'NodeViewExisting',
|
WORKFLOW = 'NodeViewExisting',
|
||||||
DEMO = 'WorkflowDemo',
|
DEMO = 'WorkflowDemo',
|
||||||
TEMPLATE_IMPORT = 'WorkflowTemplate',
|
TEMPLATE_IMPORT = 'WorkflowTemplate',
|
||||||
|
WORKFLOW_ONBOARDING = 'WorkflowOnboarding',
|
||||||
SIGNIN = 'SigninView',
|
SIGNIN = 'SigninView',
|
||||||
SIGNUP = 'SignupView',
|
SIGNUP = 'SignupView',
|
||||||
SIGNOUT = 'SignoutView',
|
SIGNOUT = 'SignoutView',
|
||||||
|
|
|
@ -878,8 +878,6 @@ export const workflowHelpers = defineComponent({
|
||||||
|
|
||||||
const workflowDataRequest: IWorkflowDataUpdate =
|
const workflowDataRequest: IWorkflowDataUpdate =
|
||||||
data || (await this.getWorkflowDataToSave());
|
data || (await this.getWorkflowDataToSave());
|
||||||
// make sure that the new ones are not active
|
|
||||||
workflowDataRequest.active = false;
|
|
||||||
const changedNodes = {} as IDataObject;
|
const changedNodes = {} as IDataObject;
|
||||||
|
|
||||||
if (resetNodeIds) {
|
if (resetNodeIds) {
|
||||||
|
|
|
@ -1560,6 +1560,7 @@
|
||||||
"tagsTableHeader.searchTags": "Search Tags",
|
"tagsTableHeader.searchTags": "Search Tags",
|
||||||
"tagsView.inUse": "{count} workflow | {count} workflows",
|
"tagsView.inUse": "{count} workflow | {count} workflows",
|
||||||
"tagsView.notBeingUsed": "Not being used",
|
"tagsView.notBeingUsed": "Not being used",
|
||||||
|
"onboarding.title": "Demo: {name}",
|
||||||
"template.buttons.goBackButton": "Go back",
|
"template.buttons.goBackButton": "Go back",
|
||||||
"template.buttons.useThisWorkflowButton": "Use this workflow",
|
"template.buttons.useThisWorkflowButton": "Use this workflow",
|
||||||
"template.details.appsInTheCollection": "This collection features",
|
"template.details.appsInTheCollection": "This collection features",
|
||||||
|
|
|
@ -41,6 +41,7 @@ import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
||||||
import SettingsExternalSecrets from './views/SettingsExternalSecrets.vue';
|
import SettingsExternalSecrets from './views/SettingsExternalSecrets.vue';
|
||||||
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||||
import WorkflowHistory from '@/views/WorkflowHistory.vue';
|
import WorkflowHistory from '@/views/WorkflowHistory.vue';
|
||||||
|
import WorkflowOnboardingView from '@/views/WorkflowOnboardingView.vue';
|
||||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
|
|
||||||
interface IRouteConfig {
|
interface IRouteConfig {
|
||||||
|
@ -57,11 +58,11 @@ interface IRouteConfig {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplatesRedirect() {
|
function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]) {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
|
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
|
||||||
if (!isTemplatesEnabled) {
|
if (!isTemplatesEnabled) {
|
||||||
return { name: VIEWS.NOT_FOUND };
|
return { name: defaultRedirect || VIEWS.NOT_FOUND };
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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',
|
path: '/workflow/new',
|
||||||
name: VIEWS.NEW_WORKFLOW,
|
name: VIEWS.NEW_WORKFLOW,
|
||||||
|
|
101
packages/editor-ui/src/stores/__tests__/workflows.spec.ts
Normal file
101
packages/editor-ui/src/stores/__tests__/workflows.spec.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
|
INodeUi,
|
||||||
ITemplatesCategory,
|
ITemplatesCategory,
|
||||||
ITemplatesCollection,
|
ITemplatesCollection,
|
||||||
ITemplatesCollectionFull,
|
ITemplatesCollectionFull,
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
getWorkflows,
|
getWorkflows,
|
||||||
getWorkflowTemplate,
|
getWorkflowTemplate,
|
||||||
} from '@/api/templates';
|
} from '@/api/templates';
|
||||||
|
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
||||||
|
|
||||||
const TEMPLATES_PAGE_SIZE = 10;
|
const TEMPLATES_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
@ -332,5 +334,19 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
||||||
const versionCli: string = settingsStore.versionCli;
|
const versionCli: string = settingsStore.versionCli;
|
||||||
return getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
return getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getFixedWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate | undefined> {
|
||||||
|
const template = await this.getWorkflowTemplate(templateId);
|
||||||
|
if (template?.workflow?.nodes) {
|
||||||
|
template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[];
|
||||||
|
template.workflow.nodes?.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
delete node.credentials;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1272,6 +1272,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
|
|
||||||
// Creates a new workflow
|
// Creates a new workflow
|
||||||
async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> {
|
async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> {
|
||||||
|
// make sure that the new ones are not active
|
||||||
|
sendData.active = false;
|
||||||
|
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
return makeRestApiRequest(
|
return makeRestApiRequest(
|
||||||
rootStore.getRestApiContext,
|
rootStore.getRestApiContext,
|
||||||
|
|
|
@ -886,7 +886,7 @@ export default defineComponent({
|
||||||
let data: IWorkflowTemplate | undefined;
|
let data: IWorkflowTemplate | undefined;
|
||||||
try {
|
try {
|
||||||
void this.$externalHooks().run('template.requested', { templateId });
|
void this.$externalHooks().run('template.requested', { templateId });
|
||||||
data = await this.templatesStore.getWorkflowTemplate(templateId);
|
data = await this.templatesStore.getFixedWorkflowTemplate(templateId);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -901,14 +901,6 @@ export default defineComponent({
|
||||||
return;
|
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;
|
this.blankRedirect = true;
|
||||||
await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
||||||
|
|
||||||
|
@ -2635,6 +2627,15 @@ export default defineComponent({
|
||||||
this.titleSet(workflow.name, 'IDLE');
|
this.titleSet(workflow.name, 'IDLE');
|
||||||
await this.openWorkflow(workflow);
|
await this.openWorkflow(workflow);
|
||||||
await this.checkAndInitDebugMode();
|
await this.checkAndInitDebugMode();
|
||||||
|
|
||||||
|
if (workflow.meta?.onboardingId) {
|
||||||
|
this.$telemetry.track(
|
||||||
|
`User opened workflow from onboarding template with ID ${workflow.meta.onboardingId}`,
|
||||||
|
{
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (this.$route.meta?.nodeView === true) {
|
} else if (this.$route.meta?.nodeView === true) {
|
||||||
// Create new workflow
|
// Create new workflow
|
||||||
|
|
68
packages/editor-ui/src/views/WorkflowOnboardingView.vue
Normal file
68
packages/editor-ui/src/views/WorkflowOnboardingView.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLoadingService, useI18n } from '@/composables';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import { useTemplatesStore, useWorkflowsStore } from '@/stores';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const loadingService = useLoadingService();
|
||||||
|
const templateStore = useTemplatesStore();
|
||||||
|
const workfowStore = useWorkflowsStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const openWorkflowTemplate = async (templateId: string) => {
|
||||||
|
try {
|
||||||
|
loadingService.startLoading();
|
||||||
|
const template = await templateStore.getFixedWorkflowTemplate(templateId);
|
||||||
|
if (!template) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const name: string = i18n.baseText('onboarding.title', {
|
||||||
|
interpolate: { name: template.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflow = await workfowStore.createNewWorkflow({
|
||||||
|
name,
|
||||||
|
connections: template.workflow.connections,
|
||||||
|
nodes: template.workflow.nodes,
|
||||||
|
pinData: template.workflow.pinData,
|
||||||
|
settings: template.workflow.settings,
|
||||||
|
meta: {
|
||||||
|
onboardingId: templateId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.replace({
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const templateId = route.params.id;
|
||||||
|
if (!templateId || typeof templateId !== 'string') {
|
||||||
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await openWorkflowTemplate(templateId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module></style>
|
|
@ -1816,6 +1816,10 @@ export interface IWorkflowSettings {
|
||||||
executionOrder?: 'v0' | 'v1';
|
executionOrder?: 'v0' | 'v1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowFEMeta {
|
||||||
|
onboardingId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkflowTestData {
|
export interface WorkflowTestData {
|
||||||
description: string;
|
description: string;
|
||||||
input: {
|
input: {
|
||||||
|
|
Loading…
Reference in a new issue