mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(editor): Improve n8n welcome experience (#3289)
* ✨ Injecting a welcome sticky note if a corresponding flag has been received from backend * 🔒 Allowing resources from `/static` route to be displayed in markown component. * ✨ Implemented image width control via markdown URLs * 💄Updating quickstart video thumbnail images. * 🔨 Updated new workflow action name and quickstart sticky name * ✨ Added quickstart menu item in the Help menu * 🔨 Moving quickstart video thumbnail to the translation file. * 🔒 Limiting http static resource requests in markdown img tags only to image files. * 🔒 Adding more file types to supported image list in markown component. * 👌 Extracting quickstart note name to constant. * 🐘 add DB migration sqlite * ⚡️ add logic for onboarding flow flag * 🐘 add postgres migration for user settings * 🐘 add mysql migration for user settings * ✨ Injecting a welcome sticky note if a corresponding flag has been received from backend * 🔒 Allowing resources from `/static` route to be displayed in markown component. * ✨ Implemented image width control via markdown URLs * 💄Updating quickstart video thumbnail images. * 🔨 Updated new workflow action name and quickstart sticky name * ✨ Added quickstart menu item in the Help menu * 🔨 Moving quickstart video thumbnail to the translation file. * 🔒 Limiting http static resource requests in markdown img tags only to image files. * 🔒 Adding more file types to supported image list in markown component. * 👌 Extracting quickstart note name to constant. * 📈 Added telemetry events to quickstart sticky note. * ⚡ Disable sticky node type from showing in expression editor * 🔨 Improving welcome video link detecton when triggering telemetry events * 👌Moved sticky links click handling logic outside of the design system, removed user and instance id from telemetry events. * 👌Improving sticky note link telemetry tracking. * 🔨 Refactoring markdown component click event logic. * 🔨 Moving bits of clicked link detection logic to Markdown component. * 💄Fixing code spacing. * remove transpileonly option * update package lock * 💄Changing the default route to `/workflow`, updating welcome sticky content. * remove hardcoded * 🐛 Fixing the onboarding threshold logic so sticky notes are skipped when counting nodes. * 👕 Fixing linting errors. Co-authored-by: Milorad Filipović <milorad.filipovic19@gmail.com> Co-authored-by: Milorad Filipović <miloradfilipovic19@gmail.com> Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Milorad Filipović <milorad@n8n.io>
This commit is contained in:
parent
68cbb78680
commit
35f2ce2359
96619
package-lock.json
generated
96619
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -179,6 +179,12 @@ export const schema = {
|
||||||
default: 'My workflow',
|
default: 'My workflow',
|
||||||
env: 'WORKFLOWS_DEFAULT_NAME',
|
env: 'WORKFLOWS_DEFAULT_NAME',
|
||||||
},
|
},
|
||||||
|
onboardingFlowDisabled: {
|
||||||
|
doc: 'Show onboarding flow in new workflow',
|
||||||
|
format: 'Boolean',
|
||||||
|
default: false,
|
||||||
|
env: 'N8N_ONBOARDING_FLOW_DISABLED',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
executions: {
|
executions: {
|
||||||
|
|
|
@ -170,7 +170,7 @@ export async function generateUniqueName(
|
||||||
|
|
||||||
// name is unique
|
// name is unique
|
||||||
if (found.length === 0) {
|
if (found.length === 0) {
|
||||||
return { name: requestedName };
|
return requestedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSuffix = found.reduce((acc, { name }) => {
|
const maxSuffix = found.reduce((acc, { name }) => {
|
||||||
|
@ -190,10 +190,10 @@ export async function generateUniqueName(
|
||||||
|
|
||||||
// name is duplicate but no numeric suffixes exist yet
|
// name is duplicate but no numeric suffixes exist yet
|
||||||
if (maxSuffix === 0) {
|
if (maxSuffix === 0) {
|
||||||
return { name: `${requestedName} 2` };
|
return `${requestedName} 2`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name: `${requestedName} ${maxSuffix + 1}` };
|
return `${requestedName} ${maxSuffix + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateEntity(
|
export async function validateEntity(
|
||||||
|
|
|
@ -475,6 +475,10 @@ export interface IPersonalizationSurveyAnswers {
|
||||||
workArea: string[] | string | null;
|
workArea: string[] | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserSettings {
|
||||||
|
isOnboarded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserManagementSettings {
|
export interface IUserManagementSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
showSetupOnFirstLoad?: boolean;
|
showSetupOnFirstLoad?: boolean;
|
||||||
|
|
|
@ -138,7 +138,7 @@ import * as TagHelpers from './TagHelpers';
|
||||||
import { InternalHooksManager } from './InternalHooksManager';
|
import { InternalHooksManager } from './InternalHooksManager';
|
||||||
import { TagEntity } from './databases/entities/TagEntity';
|
import { TagEntity } from './databases/entities/TagEntity';
|
||||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||||
import { getSharedWorkflowIds, whereClause } from './WorkflowHelpers';
|
import { getSharedWorkflowIds, isBelowOnboardingThreshold, whereClause } from './WorkflowHelpers';
|
||||||
import { getCredentialTranslationPath, getNodeTranslationPath } from './TranslationHelpers';
|
import { getCredentialTranslationPath, getNodeTranslationPath } from './TranslationHelpers';
|
||||||
import { WEBHOOK_METHODS } from './WebhookHelpers';
|
import { WEBHOOK_METHODS } from './WebhookHelpers';
|
||||||
|
|
||||||
|
@ -911,7 +911,14 @@ class App {
|
||||||
const requestedName =
|
const requestedName =
|
||||||
req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName;
|
req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName;
|
||||||
|
|
||||||
return await GenericHelpers.generateUniqueName(requestedName, 'workflow');
|
const name = await GenericHelpers.generateUniqueName(requestedName, 'workflow');
|
||||||
|
|
||||||
|
const onboardingFlowEnabled =
|
||||||
|
!config.getEnv('workflows.onboardingFlowDisabled') &&
|
||||||
|
!req.user.settings?.isOnboarded &&
|
||||||
|
(await isBelowOnboardingThreshold(req.user));
|
||||||
|
|
||||||
|
return { name, onboardingFlowEnabled };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
/* eslint-disable no-restricted-syntax */
|
/* eslint-disable no-restricted-syntax */
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { In } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -596,3 +597,52 @@ export async function getSharedWorkflowIds(user: User): Promise<number[]> {
|
||||||
|
|
||||||
return sharedWorkflows.map(({ workflow }) => workflow.id);
|
return sharedWorkflows.map(({ workflow }) => workflow.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user owns more than 15 workflows or more than 2 workflows with at least 2 nodes.
|
||||||
|
* If user does, set flag in its settings.
|
||||||
|
*/
|
||||||
|
export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {
|
||||||
|
let belowThreshold = true;
|
||||||
|
const skippedTypes = ['n8n-nodes-base.start', 'n8n-nodes-base.stickyNote'];
|
||||||
|
|
||||||
|
const workflowOwnerRole = await Db.collections.Role.findOne({
|
||||||
|
name: 'owner',
|
||||||
|
scope: 'workflow',
|
||||||
|
});
|
||||||
|
const ownedWorkflowsIds = await Db.collections.SharedWorkflow.find({
|
||||||
|
user,
|
||||||
|
role: workflowOwnerRole,
|
||||||
|
}).then((ownedWorkflows) => ownedWorkflows.map((wf) => wf.workflowId));
|
||||||
|
|
||||||
|
if (ownedWorkflowsIds.length > 15) {
|
||||||
|
belowThreshold = false;
|
||||||
|
} else {
|
||||||
|
// just fetch workflows' nodes to keep memory footprint low
|
||||||
|
const workflows = await Db.collections.Workflow.find({
|
||||||
|
where: { id: In(ownedWorkflowsIds) },
|
||||||
|
select: ['nodes'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// valid workflow: 2+ nodes without start node
|
||||||
|
const validWorkflowCount = workflows.reduce((counter, workflow) => {
|
||||||
|
if (counter <= 2 && workflow.nodes.length > 2) {
|
||||||
|
const nodes = workflow.nodes.filter((node) => !skippedTypes.includes(node.type));
|
||||||
|
if (nodes.length >= 2) {
|
||||||
|
return counter + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counter;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// more than 2 valid workflows required
|
||||||
|
belowThreshold = validWorkflowCount <= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user is above threshold --> set flag in settings
|
||||||
|
if (!belowThreshold) {
|
||||||
|
void Db.collections.User.update(user.id, { settings: { isOnboarded: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return belowThreshold;
|
||||||
|
}
|
||||||
|
|
|
@ -98,10 +98,12 @@ credentialsController.get(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.NewName): Promise<{ name: string }> => {
|
ResponseHelper.send(async (req: CredentialRequest.NewName): Promise<{ name: string }> => {
|
||||||
const { name: newName } = req.query;
|
const { name: newName } = req.query;
|
||||||
|
|
||||||
return GenericHelpers.generateUniqueName(
|
return {
|
||||||
|
name: await GenericHelpers.generateUniqueName(
|
||||||
newName ?? config.getEnv('credentials.defaultName'),
|
newName ?? config.getEnv('credentials.defaultName'),
|
||||||
'credentials',
|
'credentials',
|
||||||
);
|
),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IsEmail, IsString, Length } from 'class-validator';
|
import { IsEmail, IsString, Length } from 'class-validator';
|
||||||
import * as config from '../../../config';
|
import * as config from '../../../config';
|
||||||
import { DatabaseType, IPersonalizationSurveyAnswers } from '../..';
|
import { DatabaseType, IPersonalizationSurveyAnswers, IUserSettings } from '../..';
|
||||||
import { Role } from './Role';
|
import { Role } from './Role';
|
||||||
import { SharedWorkflow } from './SharedWorkflow';
|
import { SharedWorkflow } from './SharedWorkflow';
|
||||||
import { SharedCredentials } from './SharedCredentials';
|
import { SharedCredentials } from './SharedCredentials';
|
||||||
|
@ -102,6 +102,12 @@ export class User {
|
||||||
})
|
})
|
||||||
personalizationAnswers: IPersonalizationSurveyAnswers | null;
|
personalizationAnswers: IPersonalizationSurveyAnswers | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: resolveDataType('json') as ColumnOptions['type'],
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
settings: IUserSettings | null;
|
||||||
|
|
||||||
@ManyToOne(() => Role, (role) => role.globalForUsers, {
|
@ManyToOne(() => Role, (role) => role.globalForUsers, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import * as config from '../../../../config';
|
||||||
|
|
||||||
|
export class AddUserSettings1652367743993 implements MigrationInterface {
|
||||||
|
name = 'AddUserSettings1652367743993';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
'ALTER TABLE `' + tablePrefix + 'user` ADD COLUMN `settings` json NULL DEFAULT NULL',
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
'ALTER TABLE `' +
|
||||||
|
tablePrefix +
|
||||||
|
'user` CHANGE COLUMN `personalizationAnswers` `personalizationAnswers` json NULL DEFAULT NULL',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'user` DROP COLUMN `settings`');
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWo
|
||||||
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
|
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
|
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -30,4 +31,5 @@ export const mysqlMigrations = [
|
||||||
AddExecutionEntityIndexes1644424784709,
|
AddExecutionEntityIndexes1644424784709,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
LowerCaseUserEmail1648740597343,
|
LowerCaseUserEmail1648740597343,
|
||||||
|
AddUserSettings1652367743993,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import * as config from '../../../../config';
|
||||||
|
|
||||||
|
export class AddUserSettings1652367743993 implements MigrationInterface {
|
||||||
|
name = 'AddUserSettings1652367743993';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
const schema = config.getEnv('database.postgresdb.schema');
|
||||||
|
if (schema) {
|
||||||
|
tablePrefix = schema + '.' + tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.query(`ALTER TABLE ${tablePrefix}user ADD COLUMN settings json`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE ${tablePrefix}user ALTER COLUMN "personalizationAnswers" TYPE json USING to_jsonb("personalizationAnswers")::json;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
const schema = config.getEnv('database.postgresdb.schema');
|
||||||
|
if (schema) {
|
||||||
|
tablePrefix = schema + '.' + tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.query(`ALTER TABLE ${tablePrefix}user DROP COLUMN settings`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { AddExecutionEntityIndexes1644422880309 } from './1644422880309-AddExecu
|
||||||
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
|
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
|
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -26,4 +27,5 @@ export const postgresMigrations = [
|
||||||
IncreaseTypeVarcharLimit1646834195327,
|
IncreaseTypeVarcharLimit1646834195327,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
LowerCaseUserEmail1648740597343,
|
LowerCaseUserEmail1648740597343,
|
||||||
|
AddUserSettings1652367743993,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import * as config from '../../../../config';
|
||||||
|
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class AddUserSettings1652367743993 implements MigrationInterface {
|
||||||
|
name = 'AddUserSettings1652367743993';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query('PRAGMA foreign_keys=OFF');
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user" ("id" varchar PRIMARY KEY NOT NULL, "email" varchar(255), "firstName" varchar(32), "lastName" varchar(32), "password" varchar, "resetPasswordToken" varchar, "resetPasswordTokenExpiration" integer DEFAULT NULL, "personalizationAnswers" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "globalRoleId" integer NOT NULL, "settings" text, CONSTRAINT "FK_${tablePrefix}f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "${tablePrefix}role" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user"("id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId") SELECT "id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId" FROM "${tablePrefix}user"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}user"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "${tablePrefix}user"`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "UQ_${tablePrefix}e12875dfb3b1d92d7d7c5377e2" ON "${tablePrefix}user" ("email")`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query('PRAGMA foreign_keys=ON');
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query('PRAGMA foreign_keys=OFF');
|
||||||
|
|
||||||
|
await queryRunner.query(`ALTER TABLE "${tablePrefix}user" RENAME TO "temporary_user"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "${tablePrefix}user" ("id" varchar PRIMARY KEY NOT NULL, "email" varchar(255), "firstName" varchar(32), "lastName" varchar(32), "password" varchar, "resetPasswordToken" varchar, "resetPasswordTokenExpiration" integer DEFAULT NULL, "personalizationAnswers" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "globalRoleId" integer NOT NULL, CONSTRAINT "FK_${tablePrefix}f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "${tablePrefix}role" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "${tablePrefix}user"("id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId") SELECT "id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId" FROM "temporary_user"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "UQ_${tablePrefix}e12875dfb3b1d92d7d7c5377e2" ON "${tablePrefix}user" ("email")`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query('PRAGMA foreign_keys=ON');
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWo
|
||||||
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
|
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
|
||||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||||
|
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||||
|
|
||||||
const sqliteMigrations = [
|
const sqliteMigrations = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -26,6 +27,7 @@ const sqliteMigrations = [
|
||||||
AddExecutionEntityIndexes1644421939510,
|
AddExecutionEntityIndexes1644421939510,
|
||||||
CreateUserManagement1646992772331,
|
CreateUserManagement1646992772331,
|
||||||
LowerCaseUserEmail1648740597343,
|
LowerCaseUserEmail1648740597343,
|
||||||
|
AddUserSettings1652367743993,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
2
packages/cli/src/requests.d.ts
vendored
2
packages/cli/src/requests.d.ts
vendored
|
@ -51,7 +51,7 @@ export declare namespace WorkflowRequest {
|
||||||
|
|
||||||
type Update = AuthenticatedRequest<{ id: string }, {}, RequestBody>;
|
type Update = AuthenticatedRequest<{ id: string }, {}, RequestBody>;
|
||||||
|
|
||||||
type NewName = express.Request<{}, {}, {}, { name?: string }>;
|
type NewName = AuthenticatedRequest<{}, {}, {}, { name?: string }>;
|
||||||
|
|
||||||
type GetAll = AuthenticatedRequest<{}, {}, {}, { filter: string }>;
|
type GetAll = AuthenticatedRequest<{}, {}, {}, { filter: string }>;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
v-if="!loading"
|
v-if="!loading"
|
||||||
ref="editor"
|
ref="editor"
|
||||||
:class="$style[theme]" v-html="htmlContent"
|
:class="$style[theme]" v-html="htmlContent"
|
||||||
|
@click="onClick"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
<div v-for="(block, index) in loadingBlocks"
|
<div v-for="(block, index) in loadingBlocks"
|
||||||
|
@ -117,6 +118,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
||||||
|
const imageFilesRegex = /\.(jpeg|jpg|gif|png|webp|bmp|tif|tiff|apng|svg|avif)$/;
|
||||||
let contentToRender = this.content;
|
let contentToRender = this.content;
|
||||||
if (this.withMultiBreaks) {
|
if (this.withMultiBreaks) {
|
||||||
contentToRender = contentToRender.replaceAll('\n\n', '\n \n');
|
contentToRender = contentToRender.replaceAll('\n\n', '\n \n');
|
||||||
|
@ -129,7 +131,10 @@ export default {
|
||||||
const id = value.split('fileId:')[1];
|
const id = value.split('fileId:')[1];
|
||||||
return `src=${xss.friendlyAttrValue(imageUrls[id])}` || '';
|
return `src=${xss.friendlyAttrValue(imageUrls[id])}` || '';
|
||||||
}
|
}
|
||||||
if (!value.startsWith('https://')) {
|
// Only allow http requests to supported image files from the `static` directory
|
||||||
|
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
|
||||||
|
const isStaticImageFile = isImageFile && value.startsWith('/static/');
|
||||||
|
if (!value.startsWith('https://') && !isStaticImageFile) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +159,22 @@ export default {
|
||||||
.use(markdownTasklists, this.options.tasklists),
|
.use(markdownTasklists, this.options.tasklists),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
onClick(event) {
|
||||||
|
let clickedLink = null;
|
||||||
|
|
||||||
|
if(event.target instanceof HTMLAnchorElement) {
|
||||||
|
clickedLink = event.target;
|
||||||
|
}
|
||||||
|
if(event.target.matches('a *')) {
|
||||||
|
const parentLink = event.target.closest('a');
|
||||||
|
if(parentLink) {
|
||||||
|
clickedLink = parentLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit('markdown-click', clickedLink, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -287,6 +308,10 @@ export default {
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
||||||
|
&[src*="#full-width"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
theme="sticky"
|
theme="sticky"
|
||||||
:content="content"
|
:content="content"
|
||||||
:withMultiBreaks="true"
|
:withMultiBreaks="true"
|
||||||
|
@markdown-click="onMarkdownClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -164,6 +165,9 @@ export default mixins(Locale).extend({
|
||||||
onInput(value: string) {
|
onInput(value: string) {
|
||||||
this.$emit('input', value);
|
this.$emit('input', value);
|
||||||
},
|
},
|
||||||
|
onMarkdownClick(link, event) {
|
||||||
|
this.$emit('markdown-click', link, event);
|
||||||
|
},
|
||||||
onResize(values) {
|
onResize(values) {
|
||||||
this.$emit('resize', values);
|
this.$emit('resize', values);
|
||||||
},
|
},
|
||||||
|
|
BIN
packages/editor-ui/public/static/quickstart_thumbnail.png
Normal file
BIN
packages/editor-ui/public/static/quickstart_thumbnail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 389 KiB |
|
@ -2,6 +2,10 @@ import { IRestApiContext } from '@/Interface';
|
||||||
import { makeRestApiRequest } from './helpers';
|
import { makeRestApiRequest } from './helpers';
|
||||||
|
|
||||||
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
||||||
return await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
|
const response = await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
|
||||||
|
return {
|
||||||
|
name: response.name,
|
||||||
|
onboardingFlowEnabled: response.onboardingFlowEnabled === true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -263,6 +263,16 @@ export default mixins(
|
||||||
},
|
},
|
||||||
helpMenuItems (): object[] {
|
helpMenuItems (): object[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
id: 'quickstart',
|
||||||
|
type: 'link',
|
||||||
|
properties: {
|
||||||
|
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
|
||||||
|
title: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
|
||||||
|
icon: 'video',
|
||||||
|
newWindow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'docs',
|
id: 'docs',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
@resizestart="onResizeStart"
|
@resizestart="onResizeStart"
|
||||||
@resize="onResize"
|
@resize="onResize"
|
||||||
@resizeend="onResizeEnd"
|
@resizeend="onResizeEnd"
|
||||||
|
@markdown-click="onMarkdownClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ import { INodeUi, XYPosition } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { QUICKSTART_NOTE_NAME } from '@/constants';
|
||||||
|
|
||||||
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||||
name: 'Sticky',
|
name: 'Sticky',
|
||||||
|
@ -146,6 +148,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
this.$store.commit('setActiveNode', null);
|
this.$store.commit('setActiveNode', null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onMarkdownClick ( link:HTMLAnchorElement, event: Event ) {
|
||||||
|
if (link) {
|
||||||
|
const isOnboardingNote = this.name === QUICKSTART_NOTE_NAME;
|
||||||
|
const isWelcomeVideo = link.querySelector('img[alt="n8n quickstart video"');
|
||||||
|
const type = isOnboardingNote && isWelcomeVideo ? 'welcome_video' : isOnboardingNote && link.getAttribute('href') === '/templates' ? 'templates' : 'other';
|
||||||
|
|
||||||
|
this.$telemetry.track('User clicked note link', { type } );
|
||||||
|
}
|
||||||
|
},
|
||||||
onInputChange(content: string) {
|
onInputChange(content: string) {
|
||||||
this.setParameters({content});
|
this.setParameters({content});
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME, STICKY_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -39,6 +39,11 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
// Node types that should not be displayed in variable selector
|
||||||
|
const SKIPPED_NODE_TYPES = [
|
||||||
|
STICKY_NODE_TYPE,
|
||||||
|
];
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
)
|
)
|
||||||
|
@ -561,6 +566,10 @@ export default mixins(
|
||||||
// Skip the current node as this one get added separately
|
// Skip the current node as this one get added separately
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// If node type should be skipped, continue
|
||||||
|
if (SKIPPED_NODE_TYPES.includes(node.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
nodeOptions = [
|
nodeOptions = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const MIN_WORKFLOW_NAME_LENGTH = 1;
|
||||||
export const MAX_WORKFLOW_NAME_LENGTH = 128;
|
export const MAX_WORKFLOW_NAME_LENGTH = 128;
|
||||||
export const DUPLICATE_POSTFFIX = ' copy';
|
export const DUPLICATE_POSTFFIX = ' copy';
|
||||||
export const NODE_OUTPUT_DEFAULT_KEY = '_NODE_OUTPUT_DEFAULT_KEY_';
|
export const NODE_OUTPUT_DEFAULT_KEY = '_NODE_OUTPUT_DEFAULT_KEY_';
|
||||||
|
export const QUICKSTART_NOTE_NAME = '_QUICKSTART_NOTE_';
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
export const MAX_TAG_NAME_LENGTH = 24;
|
export const MAX_TAG_NAME_LENGTH = 24;
|
||||||
|
|
|
@ -10,18 +10,21 @@ const module: Module<IWorkflowsState, IRootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {},
|
state: {},
|
||||||
actions: {
|
actions: {
|
||||||
setNewWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>, name?: string): Promise<void> => {
|
getNewWorkflowData: async (context: ActionContext<IWorkflowsState, IRootState>, name?: string): Promise<object> => {
|
||||||
let newName = '';
|
let workflowData = {
|
||||||
|
name: '',
|
||||||
|
onboardingFlowEnabled: false,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const newWorkflow = await getNewWorkflow(context.rootGetters.getRestApiContext, name);
|
workflowData = await getNewWorkflow(context.rootGetters.getRestApiContext, name);
|
||||||
newName = newWorkflow.name;
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// in case of error, default to original name
|
// in case of error, default to original name
|
||||||
newName = name || DEFAULT_NEW_WORKFLOW_NAME;
|
workflowData.name = name || DEFAULT_NEW_WORKFLOW_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.commit('setWorkflowName', { newName }, { root: true });
|
context.commit('setWorkflowName', { newName: workflowData.name }, { root: true });
|
||||||
|
return workflowData;
|
||||||
},
|
},
|
||||||
|
|
||||||
getDuplicateCurrentWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>): Promise<string> => {
|
getDuplicateCurrentWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>): Promise<string> => {
|
||||||
|
|
|
@ -265,6 +265,7 @@
|
||||||
"mainSidebar.helpMenuItems.course": "Course",
|
"mainSidebar.helpMenuItems.course": "Course",
|
||||||
"mainSidebar.helpMenuItems.documentation": "Documentation",
|
"mainSidebar.helpMenuItems.documentation": "Documentation",
|
||||||
"mainSidebar.helpMenuItems.forum": "Forum",
|
"mainSidebar.helpMenuItems.forum": "Forum",
|
||||||
|
"mainSidebar.helpMenuItems.quickstart": "Quickstart",
|
||||||
"mainSidebar.importFromFile": "Import from File",
|
"mainSidebar.importFromFile": "Import from File",
|
||||||
"mainSidebar.importFromUrl": "Import from URL",
|
"mainSidebar.importFromUrl": "Import from URL",
|
||||||
"mainSidebar.new": "New",
|
"mainSidebar.new": "New",
|
||||||
|
@ -480,6 +481,7 @@
|
||||||
"nodeWebhooks.showMessage.title": "URL copied",
|
"nodeWebhooks.showMessage.title": "URL copied",
|
||||||
"nodeWebhooks.testUrl": "Test URL",
|
"nodeWebhooks.testUrl": "Test URL",
|
||||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||||
|
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \n\n### Quickstart video\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=RpjQTGKm-ok)\nCovers key concepts really quickly\n\n\n### Template library\nGet inspiration and learn useful techniques from our pre-built [workflow templates](/templates).",
|
||||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||||
"parameterInput.addExpression": "Add Expression",
|
"parameterInput.addExpression": "Add Expression",
|
||||||
|
|
|
@ -93,6 +93,7 @@ import {
|
||||||
faUserCircle,
|
faUserCircle,
|
||||||
faUserFriends,
|
faUserFriends,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
faVideo,
|
||||||
faStickyNote as faSolidStickyNote,
|
faStickyNote as faSolidStickyNote,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {
|
import {
|
||||||
|
@ -197,6 +198,7 @@ addIcon(faUndo);
|
||||||
addIcon(faUserCircle);
|
addIcon(faUserCircle);
|
||||||
addIcon(faUserFriends);
|
addIcon(faUserFriends);
|
||||||
addIcon(faUsers);
|
addIcon(faUsers);
|
||||||
|
addIcon(faVideo);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
||||||
|
|
|
@ -56,12 +56,6 @@ const router = new Router({
|
||||||
name: VIEWS.HOMEPAGE,
|
name: VIEWS.HOMEPAGE,
|
||||||
meta: {
|
meta: {
|
||||||
getRedirect(store: Store<IRootState>) {
|
getRedirect(store: Store<IRootState>) {
|
||||||
const isTemplatesEnabled: boolean = store.getters['settings/isTemplatesEnabled'];
|
|
||||||
const isTemplatesEndpointReachable: boolean = store.getters['settings/isTemplatesEndpointReachable'];
|
|
||||||
if (isTemplatesEnabled && isTemplatesEndpointReachable) {
|
|
||||||
return { name: VIEWS.TEMPLATES };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name: VIEWS.NEW_WORKFLOW };
|
return { name: VIEWS.NEW_WORKFLOW };
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
|
|
|
@ -147,7 +147,7 @@ import {
|
||||||
} from 'jsplumb';
|
} from 'jsplumb';
|
||||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
||||||
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, QUICKSTART_NOTE_NAME, START_NODE_TYPE, STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
|
@ -596,7 +596,7 @@ export default mixins(
|
||||||
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
||||||
|
|
||||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||||
await this.$store.dispatch('workflows/setNewWorkflowName', data.name);
|
await this.$store.dispatch('workflows/getNewWorkflowData', data.name);
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
@ -1836,7 +1836,8 @@ export default mixins(
|
||||||
},
|
},
|
||||||
async newWorkflow (): Promise<void> {
|
async newWorkflow (): Promise<void> {
|
||||||
await this.resetWorkspace();
|
await this.resetWorkspace();
|
||||||
await this.$store.dispatch('workflows/setNewWorkflowName');
|
const newWorkflow = await this.$store.dispatch('workflows/getNewWorkflowData');
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', false);
|
this.$store.commit('setStateDirty', false);
|
||||||
|
|
||||||
await this.addNodes([{...CanvasHelpers.DEFAULT_START_NODE}]);
|
await this.addNodes([{...CanvasHelpers.DEFAULT_START_NODE}]);
|
||||||
|
@ -1848,6 +1849,24 @@ export default mixins(
|
||||||
this.setZoomLevel(1);
|
this.setZoomLevel(1);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0]});
|
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0]});
|
||||||
|
// For novice users (onboardingFlowEnabled == true)
|
||||||
|
// Inject welcome sticky note and zoom to fit
|
||||||
|
if(newWorkflow.onboardingFlowEnabled) {
|
||||||
|
this.$nextTick(async () => {
|
||||||
|
await this.addNodes([
|
||||||
|
{
|
||||||
|
...CanvasHelpers.WELCOME_STICKY_NODE,
|
||||||
|
parameters: {
|
||||||
|
// Use parameters from the template but add translated content
|
||||||
|
...CanvasHelpers.WELCOME_STICKY_NODE.parameters,
|
||||||
|
content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.zoomToFit();
|
||||||
|
this.$telemetry.track('welcome node inserted');
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
async initView (): Promise<void> {
|
async initView (): Promise<void> {
|
||||||
|
@ -2213,7 +2232,13 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
if(node.type === STICKY_NODE_TYPE) {
|
if(node.type === STICKY_NODE_TYPE) {
|
||||||
this.$telemetry.track('User deleted workflow note', { workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track(
|
||||||
|
'User deleted workflow note',
|
||||||
|
{
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
is_welcome_note: node.name === QUICKSTART_NOTE_NAME,
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$externalHooks().run('node.deleteNode', { node });
|
this.$externalHooks().run('node.deleteNode', { node });
|
||||||
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getStyleTokenValue, isNumber } from "@/components/helpers";
|
import { getStyleTokenValue, isNumber } from "@/components/helpers";
|
||||||
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE } from "@/constants";
|
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from "@/constants";
|
||||||
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||||
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||||
import {
|
import {
|
||||||
|
@ -48,6 +48,20 @@ export const DEFAULT_START_NODE = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WELCOME_STICKY_NODE = {
|
||||||
|
name: QUICKSTART_NOTE_NAME,
|
||||||
|
type: STICKY_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [
|
||||||
|
-240,
|
||||||
|
140,
|
||||||
|
] as XYPosition,
|
||||||
|
parameters: {
|
||||||
|
height: 440,
|
||||||
|
width: 380,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
|
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
|
||||||
cornerRadius: 12,
|
cornerRadius: 12,
|
||||||
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||||
|
|
Loading…
Reference in a new issue