refactor: Add node IDs (#3788)

* update type

* add id to new nodes

* update paste/import behavior

* update duplicate/copy

* update duplicate workflow

* update import functions + templates

* add instance id on copy

* on download add instance id

* simplify for testing

* update telemetry events

* add ids to nodegraph

* not if same instance

* update spacing

* fix tests

* update tests

* add uuid

* fix tests

update tests

add uuid

fix ts issue

* fix telemetry event

* update workflow import

* update public api

* add sqlit migration

* on workflow update

* add psql migration

* add mysql migration

* revert to title

* fix telemetry bug

* remove console log

* remove migration logs

* fix copy/paste bug

* replace node index with node id

* remove console log

* address PR feedback

* address comment

* fix type issue

* fix select

* update schema

* fix ts issue

* update tel helpers

* fix eslint issues
This commit is contained in:
Mutasem Aldmour
2022-08-03 13:06:53 +02:00
committed by GitHub
parent b5ea666ecf
commit 679a443a0c
40 changed files with 602 additions and 157 deletions

View File

@@ -17,6 +17,7 @@ import fs from 'fs';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { UserSettings } from 'n8n-core'; import { UserSettings } from 'n8n-core';
import { EntityManager, getConnection } from 'typeorm'; import { EntityManager, getConnection } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { getLogger } from '../../src/Logger'; import { getLogger } from '../../src/Logger';
import { Db, ICredentialsDb, IWorkflowToImport } from '../../src'; import { Db, ICredentialsDb, IWorkflowToImport } from '../../src';
import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow'; import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow';
@@ -129,6 +130,11 @@ export class ImportWorkflowsCommand extends Command {
if (credentials.length > 0) { if (credentials.length > 0) {
workflow.nodes.forEach((node: INode) => { workflow.nodes.forEach((node: INode) => {
this.transformCredentials(node, credentials); this.transformCredentials(node, credentials);
if (!node.id) {
// eslint-disable-next-line no-param-reassign
node.id = uuid();
}
}); });
} }
@@ -157,6 +163,11 @@ export class ImportWorkflowsCommand extends Command {
if (credentials.length > 0) { if (credentials.length > 0) {
workflow.nodes.forEach((node: INode) => { workflow.nodes.forEach((node: INode) => {
this.transformCredentials(node, credentials); this.transformCredentials(node, credentials);
if (!node.id) {
// eslint-disable-next-line no-param-reassign
node.id = uuid();
}
}); });
} }

View File

@@ -586,6 +586,7 @@ export class CredentialsHelper extends ICredentialsHelper {
} }
const node: INode = { const node: INode = {
id: 'temp',
parameters: {}, parameters: {},
name: 'Temp-Node', name: 'Temp-Node',
type: nodeType.description.name, type: nodeType.description.name,

View File

@@ -1,6 +1,9 @@
type: object type: object
additionalProperties: false additionalProperties: false
properties: properties:
id:
type: string
example: 0f5532f9-36ba-4bef-86c7-30d607400b15
name: name:
type: string type: string
example: Jira example: Jira

View File

@@ -7,7 +7,7 @@ import config = require('../../../../../config');
import { WorkflowEntity } from '../../../../databases/entities/WorkflowEntity'; import { WorkflowEntity } from '../../../../databases/entities/WorkflowEntity';
import { InternalHooksManager } from '../../../../InternalHooksManager'; import { InternalHooksManager } from '../../../../InternalHooksManager';
import { externalHooks } from '../../../../Server'; import { externalHooks } from '../../../../Server';
import { replaceInvalidCredentials } from '../../../../WorkflowHelpers'; import { addNodeIds, replaceInvalidCredentials } from '../../../../WorkflowHelpers';
import { WorkflowRequest } from '../../../types'; import { WorkflowRequest } from '../../../types';
import { authorize, validCursor } from '../../shared/middlewares/global.middleware'; import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
import { encodeNextCursor } from '../../shared/services/pagination.service'; import { encodeNextCursor } from '../../shared/services/pagination.service';
@@ -42,6 +42,8 @@ export = {
await replaceInvalidCredentials(workflow); await replaceInvalidCredentials(workflow);
addNodeIds(workflow);
const role = await getWorkflowOwnerRole(); const role = await getWorkflowOwnerRole();
const createdWorkflow = await createWorkflow(workflow, req.user, role); const createdWorkflow = await createWorkflow(workflow, req.user, role);
@@ -186,6 +188,7 @@ export = {
} }
await replaceInvalidCredentials(updateData); await replaceInvalidCredentials(updateData);
addNodeIds(updateData);
const workflowRunner = ActiveWorkflowRunner.getInstance(); const workflowRunner = ActiveWorkflowRunner.getInstance();

View File

@@ -1,6 +1,7 @@
import { FindManyOptions, In, UpdateResult } from 'typeorm'; import { FindManyOptions, In, UpdateResult } from 'typeorm';
import intersection from 'lodash.intersection'; import intersection from 'lodash.intersection';
import type { INode } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import { Db } from '../../../..'; import { Db } from '../../../..';
import { User } from '../../../../databases/entities/User'; import { User } from '../../../../databases/entities/User';
@@ -133,6 +134,7 @@ export function hasStartNode(workflow: WorkflowEntity): boolean {
export function getStartNode(): INode { export function getStartNode(): INode {
return { return {
id: uuid(),
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',

View File

@@ -924,6 +924,8 @@ class App {
// check credentials for old format // check credentials for old format
await WorkflowHelpers.replaceInvalidCredentials(updateData); await WorkflowHelpers.replaceInvalidCredentials(updateData);
WorkflowHelpers.addNodeIds(updateData);
await this.externalHooks.run('workflow.update', [updateData]); await this.externalHooks.run('workflow.update', [updateData]);
if (shared.workflow.active) { if (shared.workflow.active) {

View File

@@ -21,6 +21,7 @@ import {
LoggerProxy as Logger, LoggerProxy as Logger,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { import {
CredentialTypes, CredentialTypes,
@@ -474,6 +475,22 @@ export async function getStaticDataById(workflowId: string | number) {
return workflowData.staticData || {}; return workflowData.staticData || {};
} }
/**
* Set node ids if not already set
*
* @param workflow
*/
export function addNodeIds(workflow: WorkflowEntity) {
const { nodes } = workflow;
if (!nodes) return;
nodes.forEach((node) => {
if (!node.id) {
node.id = uuid();
}
});
}
// Checking if credentials of old format are in use and run a DB check if they might exist uniquely // Checking if credentials of old format are in use and run a DB check if they might exist uniquely
export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise<WorkflowEntity> { export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise<WorkflowEntity> {
const { nodes } = workflow; const { nodes } = workflow;

View File

@@ -43,6 +43,8 @@ workflowsController.post(
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
WorkflowHelpers.addNodeIds(newWorkflow);
let savedWorkflow: undefined | WorkflowEntity; let savedWorkflow: undefined | WorkflowEntity;
await Db.transaction(async (transactionManager) => { await Db.transaction(async (transactionManager) => {

View File

@@ -0,0 +1,76 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
import { runChunked } from '../../utils/migrationHelpers';
import { v4 as uuid } from 'uuid';
// add node ids in workflow objects
export class AddNodeIds1658932910559 implements MigrationInterface {
name = 'AddNodeIds1658932910559';
public async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');
const workflowsQuery = `
SELECT id, nodes
FROM ${tablePrefix}workflow_entity
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = workflow.nodes;
// @ts-ignore
nodes.forEach((node) => {
if (!node.id) {
node.id = uuid();
}
});
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
}
public async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');
const workflowsQuery = `
SELECT id, nodes
FROM ${tablePrefix}workflow_entity
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = workflow.nodes;
// @ts-ignore
nodes.forEach((node) => delete node.id );
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
}
}

View File

@@ -17,6 +17,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
import { CommunityNodes1652254514003 } from './1652254514003-CommunityNodes'; import { CommunityNodes1652254514003 } from './1652254514003-CommunityNodes';
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
import { IntroducePinData1654090101303 } from './1654090101303-IntroducePinData'; import { IntroducePinData1654090101303 } from './1654090101303-IntroducePinData';
import { AddNodeIds1658932910559 } from './1658932910559-AddNodeIds';
export const mysqlMigrations = [ export const mysqlMigrations = [
InitialMigration1588157391238, InitialMigration1588157391238,
@@ -38,4 +39,5 @@ export const mysqlMigrations = [
CommunityNodes1652254514003, CommunityNodes1652254514003,
AddAPIKeyColumn1652905585850, AddAPIKeyColumn1652905585850,
IntroducePinData1654090101303, IntroducePinData1654090101303,
AddNodeIds1658932910559,
]; ];

View File

@@ -0,0 +1,88 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
import { runChunked } from '../../utils/migrationHelpers';
import { v4 as uuid } from 'uuid';
// add node ids in workflow objects
export class AddNodeIds1658932090381 implements MigrationInterface {
name = 'AddNodeIds1658932090381';
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(`SET search_path TO ${schema};`);
const workflowsQuery = `
SELECT id, nodes
FROM ${tablePrefix}workflow_entity
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = workflow.nodes;
// @ts-ignore
nodes.forEach((node) => {
if (!node.id) {
node.id = uuid();
}
});
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
}
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(`SET search_path TO ${schema};`);
const workflowsQuery = `
SELECT id, nodes
FROM ${tablePrefix}workflow_entity
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = workflow.nodes;
// @ts-ignore
nodes.forEach((node) => delete node.id );
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
}
}

View File

@@ -15,6 +15,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
import { CommunityNodes1652254514002 } from './1652254514002-CommunityNodes'; import { CommunityNodes1652254514002 } from './1652254514002-CommunityNodes';
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
import { IntroducePinData1654090467022 } from './1654090467022-IntroducePinData'; import { IntroducePinData1654090467022 } from './1654090467022-IntroducePinData';
import { AddNodeIds1658932090381 } from './1658932090381-AddNodeIds';
export const postgresMigrations = [ export const postgresMigrations = [
InitialMigration1587669153312, InitialMigration1587669153312,
@@ -34,4 +35,5 @@ export const postgresMigrations = [
CommunityNodes1652254514002, CommunityNodes1652254514002,
AddAPIKeyColumn1652905585850, AddAPIKeyColumn1652905585850,
IntroducePinData1654090467022, IntroducePinData1654090467022,
AddNodeIds1658932090381,
]; ];

View File

@@ -0,0 +1,82 @@
import { INode } from 'n8n-workflow';
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
import { runChunked } from '../../utils/migrationHelpers';
import { v4 as uuid } from 'uuid';
// add node ids in workflow objects
export class AddNodeIds1658930531669 implements MigrationInterface {
name = 'AddNodeIds1658930531669';
public async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);
const tablePrefix = config.getEnv('database.tablePrefix');
const workflowsQuery = `
SELECT id, nodes
FROM "${tablePrefix}workflow_entity"
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = JSON.parse(workflow.nodes);
nodes.forEach((node: INode) => {
if (!node.id) {
node.id = uuid();
}
});
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE "${tablePrefix}workflow_entity"
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
logMigrationEnd(this.name);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');
const workflowsQuery = `
SELECT id, nodes
FROM "${tablePrefix}workflow_entity"
`;
// @ts-ignore
await runChunked(queryRunner, workflowsQuery, (workflows) => {
workflows.forEach(async (workflow) => {
const nodes = JSON.parse(workflow.nodes);
// @ts-ignore
nodes.forEach((node) => delete node.id );
const [updateQuery, updateParams] =
queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE "${tablePrefix}workflow_entity"
SET nodes = :nodes
WHERE id = '${workflow.id}'
`,
{ nodes: JSON.stringify(nodes) },
{},
);
queryRunner.query(updateQuery, updateParams);
});
});
}
}

View File

@@ -14,6 +14,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
import { CommunityNodes1652254514001 } from './1652254514001-CommunityNodes' import { CommunityNodes1652254514001 } from './1652254514001-CommunityNodes'
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn'; import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData'; import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData';
import { AddNodeIds1658930531669 } from './1658930531669-AddNodeIds';
const sqliteMigrations = [ const sqliteMigrations = [
InitialMigration1588102412422, InitialMigration1588102412422,
@@ -32,6 +33,7 @@ const sqliteMigrations = [
CommunityNodes1652254514001, CommunityNodes1652254514001,
AddAPIKeyColumn1652905585850, AddAPIKeyColumn1652905585850,
IntroducePinData1654089251344, IntroducePinData1654089251344,
AddNodeIds1658930531669,
]; ];
export { sqliteMigrations }; export { sqliteMigrations };

View File

@@ -951,6 +951,7 @@ test('POST /workflows should create workflow', async () => {
name: 'testing', name: 'testing',
nodes: [ nodes: [
{ {
id: 'uuid-1234',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1047,6 +1048,7 @@ test('PUT /workflows/:id should fail due to non-existing workflow', async () =>
name: 'testing', name: 'testing',
nodes: [ nodes: [
{ {
id: 'uuid-1234',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1082,6 +1084,7 @@ test('PUT /workflows/:id should fail due to invalid body', async () => {
const response = await authOwnerAgent.put(`/workflows/1`).send({ const response = await authOwnerAgent.put(`/workflows/1`).send({
nodes: [ nodes: [
{ {
id: 'uuid-1234',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1120,6 +1123,7 @@ test('PUT /workflows/:id should update workflow', async () => {
name: 'name updated', name: 'name updated',
nodes: [ nodes: [
{ {
id: 'uuid-1234',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1127,6 +1131,7 @@ test('PUT /workflows/:id should update workflow', async () => {
position: [240, 300], position: [240, 300],
}, },
{ {
id: 'uuid-1234',
parameters: {}, parameters: {},
name: 'Cron', name: 'Cron',
type: 'n8n-nodes-base.cron', type: 'n8n-nodes-base.cron',
@@ -1195,6 +1200,7 @@ test('PUT /workflows/:id should update non-owned workflow if owner', async () =>
name: 'name owner updated', name: 'name owner updated',
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1202,6 +1208,7 @@ test('PUT /workflows/:id should update non-owned workflow if owner', async () =>
position: [240, 300], position: [240, 300],
}, },
{ {
id: 'uuid-2',
parameters: {}, parameters: {},
name: 'Cron', name: 'Cron',
type: 'n8n-nodes-base.cron', type: 'n8n-nodes-base.cron',

View File

@@ -522,6 +522,7 @@ export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, u
name: name ?? 'test workflow', name: name ?? 'test workflow',
nodes: nodes ?? [ nodes: nodes ?? [
{ {
id: 'uuid-1234',
name: 'Start', name: 'Start',
parameters: {}, parameters: {},
position: [-20, 260], position: [-20, 260],
@@ -555,6 +556,7 @@ export async function createWorkflowWithTrigger(
{ {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -562,6 +564,7 @@ export async function createWorkflowWithTrigger(
position: [240, 300], position: [240, 300],
}, },
{ {
id: 'uuid-2',
parameters: { triggerTimes: { item: [{ mode: 'everyMinute' }] } }, parameters: { triggerTimes: { item: [{ mode: 'everyMinute' }] } },
name: 'Cron', name: 'Cron',
type: 'n8n-nodes-base.cron', type: 'n8n-nodes-base.cron',
@@ -569,6 +572,7 @@ export async function createWorkflowWithTrigger(
position: [500, 300], position: [500, 300],
}, },
{ {
id: 'uuid-3',
parameters: { options: {} }, parameters: { options: {} },
name: 'Set', name: 'Set',
type: 'n8n-nodes-base.set', type: 'n8n-nodes-base.set',

View File

@@ -91,6 +91,7 @@ function makeWorkflow({ withPinData }: { withPinData: boolean }) {
workflow.connections = {}; workflow.connections = {};
workflow.nodes = [ workflow.nodes = [
{ {
id: 'uuid-1234',
name: 'Spotify', name: 'Spotify',
type: 'n8n-nodes-base.spotify', type: 'n8n-nodes-base.spotify',
parameters: { resource: 'track', operation: 'get', id: '123' }, parameters: { resource: 'track', operation: 'get', id: '123' },

View File

@@ -194,6 +194,7 @@ describe('CredentialsHelper', () => {
]; ];
const node: INode = { const node: INode = {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'test', name: 'test',
type: 'test.set', type: 'test.set',

View File

@@ -58,6 +58,7 @@ export class LoadNodeParameterOptions {
const nodeData: INode = { const nodeData: INode = {
parameters: currentNodeParameters, parameters: currentNodeParameters,
id: 'uuid-1234',
name: TEMP_NODE_NAME, name: TEMP_NODE_NAME,
type: nodeTypeNameAndVersion.name, type: nodeTypeNameAndVersion.name,
typeVersion: nodeTypeNameAndVersion.version, typeVersion: nodeTypeNameAndVersion.version,

View File

@@ -37,6 +37,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -44,6 +45,7 @@ describe('WorkflowExecute', () => {
position: [100, 300], position: [100, 300],
}, },
{ {
id: 'uuid-2',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -96,6 +98,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -103,6 +106,7 @@ describe('WorkflowExecute', () => {
position: [100, 300], position: [100, 300],
}, },
{ {
id: 'uuid-2',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -119,6 +123,7 @@ describe('WorkflowExecute', () => {
position: [300, 250], position: [300, 250],
}, },
{ {
id: 'uuid-3',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -200,6 +205,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: { parameters: {
mode: 'passThrough', mode: 'passThrough',
}, },
@@ -209,6 +215,7 @@ describe('WorkflowExecute', () => {
position: [1150, 500], position: [1150, 500],
}, },
{ {
id: 'uuid-2',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -225,6 +232,7 @@ describe('WorkflowExecute', () => {
position: [290, 400], position: [290, 400],
}, },
{ {
id: 'uuid-3',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -241,6 +249,7 @@ describe('WorkflowExecute', () => {
position: [850, 200], position: [850, 200],
}, },
{ {
id: 'uuid-4',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -257,6 +266,7 @@ describe('WorkflowExecute', () => {
position: [650, 200], position: [650, 200],
}, },
{ {
id: 'uuid-5',
parameters: { parameters: {
mode: 'passThrough', mode: 'passThrough',
}, },
@@ -266,6 +276,7 @@ describe('WorkflowExecute', () => {
position: [1150, 500], position: [1150, 500],
}, },
{ {
id: 'uuid-6',
parameters: {}, parameters: {},
name: 'Merge3', name: 'Merge3',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -273,6 +284,7 @@ describe('WorkflowExecute', () => {
position: [1000, 400], position: [1000, 400],
}, },
{ {
id: 'uuid-7',
parameters: { parameters: {
mode: 'passThrough', mode: 'passThrough',
output: 'input2', output: 'input2',
@@ -283,6 +295,7 @@ describe('WorkflowExecute', () => {
position: [700, 400], position: [700, 400],
}, },
{ {
id: 'uuid-8',
parameters: {}, parameters: {},
name: 'Merge1', name: 'Merge1',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -290,6 +303,7 @@ describe('WorkflowExecute', () => {
position: [500, 300], position: [500, 300],
}, },
{ {
id: 'uuid-9',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -306,6 +320,7 @@ describe('WorkflowExecute', () => {
position: [300, 200], position: [300, 200],
}, },
{ {
id: 'uuid-10',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -526,6 +541,7 @@ describe('WorkflowExecute', () => {
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [250, 450], position: [250, 450],
}, },
{ {
@@ -543,6 +559,7 @@ describe('WorkflowExecute', () => {
name: 'IF', name: 'IF',
type: 'n8n-nodes-base.if', type: 'n8n-nodes-base.if',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [650, 350], position: [650, 350],
}, },
{ {
@@ -550,6 +567,7 @@ describe('WorkflowExecute', () => {
name: 'Merge1', name: 'Merge1',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [1150, 450], position: [1150, 450],
}, },
{ {
@@ -567,6 +585,7 @@ describe('WorkflowExecute', () => {
name: 'Set1', name: 'Set1',
type: 'n8n-nodes-base.set', type: 'n8n-nodes-base.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-4',
position: [450, 450], position: [450, 450],
}, },
{ {
@@ -584,6 +603,7 @@ describe('WorkflowExecute', () => {
name: 'Set2', name: 'Set2',
type: 'n8n-nodes-base.set', type: 'n8n-nodes-base.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [800, 250], position: [800, 250],
}, },
], ],
@@ -672,6 +692,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -679,6 +700,7 @@ describe('WorkflowExecute', () => {
position: [250, 300], position: [250, 300],
}, },
{ {
id: 'uuid-2',
parameters: {}, parameters: {},
name: 'Merge', name: 'Merge',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -686,6 +708,7 @@ describe('WorkflowExecute', () => {
position: [800, 450], position: [800, 450],
}, },
{ {
id: 'uuid-3',
parameters: {}, parameters: {},
name: 'Merge1', name: 'Merge1',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -693,6 +716,7 @@ describe('WorkflowExecute', () => {
position: [1000, 300], position: [1000, 300],
}, },
{ {
id: 'uuid-4',
parameters: { parameters: {
conditions: { conditions: {
boolean: [ boolean: [
@@ -716,6 +740,7 @@ describe('WorkflowExecute', () => {
alwaysOutputData: false, alwaysOutputData: false,
}, },
{ {
id: 'uuid-5',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -738,6 +763,7 @@ describe('WorkflowExecute', () => {
position: [450, 300], position: [450, 300],
}, },
{ {
id: 'uuid-6',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -761,6 +787,7 @@ describe('WorkflowExecute', () => {
position: [450, 450], position: [450, 450],
}, },
{ {
id: 'uuid-7',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -889,6 +916,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -896,6 +924,7 @@ describe('WorkflowExecute', () => {
position: [250, 300], position: [250, 300],
}, },
{ {
id: 'uuid-2',
parameters: { parameters: {
conditions: { conditions: {
number: [ number: [
@@ -913,6 +942,7 @@ describe('WorkflowExecute', () => {
position: [650, 300], position: [650, 300],
}, },
{ {
id: 'uuid-3',
parameters: { parameters: {
values: { values: {
string: [], string: [],
@@ -931,6 +961,7 @@ describe('WorkflowExecute', () => {
position: [850, 450], position: [850, 450],
}, },
{ {
id: 'uuid-4',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -948,6 +979,7 @@ describe('WorkflowExecute', () => {
position: [450, 300], position: [450, 300],
}, },
{ {
id: 'uuid-5',
parameters: {}, parameters: {},
name: 'Merge', name: 'Merge',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -1034,6 +1066,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1041,6 +1074,7 @@ describe('WorkflowExecute', () => {
position: [250, 300], position: [250, 300],
}, },
{ {
id: 'uuid-2',
parameters: { parameters: {
values: { values: {
number: [ number: [
@@ -1057,6 +1091,7 @@ describe('WorkflowExecute', () => {
position: [450, 300], position: [450, 300],
}, },
{ {
id: 'uuid-3',
parameters: {}, parameters: {},
name: 'Merge', name: 'Merge',
type: 'n8n-nodes-base.merge', type: 'n8n-nodes-base.merge',
@@ -1064,6 +1099,7 @@ describe('WorkflowExecute', () => {
position: [1050, 250], position: [1050, 250],
}, },
{ {
id: 'uuid-4',
parameters: { parameters: {
conditions: { conditions: {
number: [ number: [
@@ -1081,6 +1117,7 @@ describe('WorkflowExecute', () => {
position: [650, 300], position: [650, 300],
}, },
{ {
id: 'uuid-5',
parameters: {}, parameters: {},
name: 'NoOpTrue', name: 'NoOpTrue',
type: 'n8n-nodes-base.noOp', type: 'n8n-nodes-base.noOp',
@@ -1088,6 +1125,7 @@ describe('WorkflowExecute', () => {
position: [850, 150], position: [850, 150],
}, },
{ {
id: 'uuid-6',
parameters: {}, parameters: {},
name: 'NoOpFalse', name: 'NoOpFalse',
type: 'n8n-nodes-base.noOp', type: 'n8n-nodes-base.noOp',
@@ -1177,6 +1215,7 @@ describe('WorkflowExecute', () => {
workflowData: { workflowData: {
nodes: [ nodes: [
{ {
id: 'uuid-1',
parameters: {}, parameters: {},
name: 'Start', name: 'Start',
type: 'n8n-nodes-base.start', type: 'n8n-nodes-base.start',
@@ -1184,6 +1223,7 @@ describe('WorkflowExecute', () => {
position: [240, 300], position: [240, 300],
}, },
{ {
id: 'uuid-2',
parameters: {}, parameters: {},
name: 'VersionTest1a', name: 'VersionTest1a',
type: 'n8n-nodes-base.versionTest', type: 'n8n-nodes-base.versionTest',
@@ -1191,6 +1231,7 @@ describe('WorkflowExecute', () => {
position: [460, 300], position: [460, 300],
}, },
{ {
id: 'uuid-3',
parameters: { parameters: {
versionTest: 11, versionTest: 11,
}, },
@@ -1200,6 +1241,7 @@ describe('WorkflowExecute', () => {
position: [680, 300], position: [680, 300],
}, },
{ {
id: 'uuid-4',
parameters: {}, parameters: {},
name: 'VersionTest2a', name: 'VersionTest2a',
type: 'n8n-nodes-base.versionTest', type: 'n8n-nodes-base.versionTest',
@@ -1207,6 +1249,7 @@ describe('WorkflowExecute', () => {
position: [880, 300], position: [880, 300],
}, },
{ {
id: 'uuid-5',
parameters: { parameters: {
versionTest: 22, versionTest: 22,
}, },

View File

@@ -259,6 +259,12 @@ export interface IWorkflowDataUpdate {
pinData?: IPinData; pinData?: IPinData;
} }
export interface IWorkflowToShare extends IWorkflowDataUpdate {
meta?: {
instanceId: string;
};
}
export interface IWorkflowTemplate { export interface IWorkflowTemplate {
id: number; id: number;
name: string; name: string;
@@ -866,7 +872,6 @@ export interface IRootState {
workflowExecutionData: IExecutionResponse | null; workflowExecutionData: IExecutionResponse | null;
lastSelectedNode: string | null; lastSelectedNode: string | null;
lastSelectedNodeOutputIndex: number | null; lastSelectedNodeOutputIndex: number | null;
nodeIndex: Array<string | null>;
nodeViewOffsetPosition: XYPosition; nodeViewOffsetPosition: XYPosition;
nodeViewMoveInProgress: boolean; nodeViewMoveInProgress: boolean;
selectedNodes: INodeUi[]; selectedNodes: INodeUi[];

View File

@@ -117,7 +117,7 @@ export default mixins(showMessage, workflowHelpers).extend({
this.$data.isSaving = true; this.$data.isSaving = true;
const saved = await this.saveAsNewWorkflow({name, tags: this.currentTagIds, resetWebhookUrls: true, openInNewWindow: true}); const saved = await this.saveAsNewWorkflow({name, tags: this.currentTagIds, resetWebhookUrls: true, openInNewWindow: true, resetNodeIds: true});
if (saved) { if (saved) {
this.closeDialog(); this.closeDialog();

View File

@@ -183,7 +183,7 @@ import {
IExecutionResponse, IExecutionResponse,
IWorkflowDataUpdate, IWorkflowDataUpdate,
IMenuItem, IMenuItem,
IUser, IWorkflowToShare,
} from '../Interface'; } from '../Interface';
import ExecutionsList from '@/components/ExecutionsList.vue'; import ExecutionsList from '@/components/ExecutionsList.vue';
@@ -442,7 +442,6 @@ export default mixins(
return; return;
} }
this.$telemetry.track('User imported workflow', { source: 'file', workflow_id: this.$store.getters.workflowId });
this.$root.$emit('importWorkflowData', { data: worflowData }); this.$root.$emit('importWorkflowData', { data: worflowData });
}; };
@@ -513,8 +512,11 @@ export default mixins(
data.id = parseInt(data.id, 10); data.id = parseInt(data.id, 10);
} }
const exportData: IWorkflowDataUpdate = { const exportData: IWorkflowToShare = {
...data, ...data,
meta: {
instanceId: this.$store.getters.instanceId,
},
tags: (tags || []).map(tagId => { tags: (tags || []).map(tagId => {
const {usageCount, ...tag} = this.$store.getters["tags/getTagById"](tagId); const {usageCount, ...tag} = this.$store.getters["tags/getTagById"](tagId);

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="node-wrapper" :style="nodePosition"> <div class="node-wrapper" :style="nodePosition" :id="nodeId">
<div class="select-background" v-show="isSelected"></div> <div class="select-background" v-show="isSelected"></div>
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name"> <div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd"> <div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="sticky-wrapper" :style="stickyPosition"> <div class="sticky-wrapper" :style="stickyPosition" :id="nodeId">
<div <div
:class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}"
:style="stickySize" :style="stickySize"
@@ -18,7 +18,7 @@
:height="node.parameters.height" :height="node.parameters.height"
:width="node.parameters.width" :width="node.parameters.width"
:scale="nodeViewScale" :scale="nodeViewScale"
:id="nodeIndex" :id="node.id"
:readOnly="isReadOnly" :readOnly="isReadOnly"
:defaultText="defaultText" :defaultText="defaultText"
:editMode="isActive && !isReadOnly" :editMode="isActive && !isReadOnly"
@@ -165,9 +165,9 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
if (!this.isSelected && this.node) { if (!this.isSelected && this.node) {
this.$emit('nodeSelected', this.node.name, false, true); this.$emit('nodeSelected', this.node.name, false, true);
} }
const nodeIndex = this.$store.getters.getNodeIndex(this.data.name); if (this.node) {
const nodeIdName = `node-${nodeIndex}`; this.instance.destroyDraggable(this.node.id); // todo avoid destroying if possible
this.instance.destroyDraggable(nodeIdName); // todo }
}, },
onResize({height, width, dX, dY}: { width: number, height: number, dX: number, dY: number }) { onResize({height, width, dX, dY}: { width: number, height: number, dX: number, dY: number }) {
if (!this.node) { if (!this.node) {

View File

@@ -3,12 +3,10 @@ import { INodeUi, XYPosition } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { getMousePosition, getRelativePosition } from '@/views/canvasHelpers'; import { getMousePosition, getRelativePosition } from '@/views/canvasHelpers';
export const mouseSelect = mixins( export const mouseSelect = mixins(
deviceSupportHelpers, deviceSupportHelpers,
nodeIndex,
).extend({ ).extend({
data () { data () {
return { return {
@@ -171,18 +169,15 @@ export const mouseSelect = mixins(
this.updateSelectBox(e); this.updateSelectBox(e);
}, },
nodeDeselected (node: INodeUi) { nodeDeselected (node: INodeUi) {
this.$store.commit('removeNodeFromSelection', node); this.$store.commit('removeNodeFromSelection', node);
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
// @ts-ignore // @ts-ignore
this.instance.removeFromDragSelection(nodeElement); this.instance.removeFromDragSelection(node.id);
}, },
nodeSelected (node: INodeUi) { nodeSelected (node: INodeUi) {
this.$store.commit('addSelectedNode', node); this.$store.commit('addSelectedNode', node);
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
// @ts-ignore // @ts-ignore
this.instance.addToDragSelection(nodeElement); this.instance.addToDragSelection(node.id);
}, },
deselectAllNodes () { deselectAllNodes () {
// @ts-ignore // @ts-ignore

View File

@@ -2,12 +2,10 @@ import mixins from 'vue-typed-mixins';
// @ts-ignore // @ts-ignore
import normalizeWheel from 'normalize-wheel'; import normalizeWheel from 'normalize-wheel';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { getMousePosition } from '@/views/canvasHelpers'; import { getMousePosition } from '@/views/canvasHelpers';
export const moveNodeWorkflow = mixins( export const moveNodeWorkflow = mixins(
deviceSupportHelpers, deviceSupportHelpers,
nodeIndex,
).extend({ ).extend({
data () { data () {
return { return {

View File

@@ -3,8 +3,7 @@ import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import * as CanvasHelpers from '@/views/canvasHelpers'; import * as CanvasHelpers from '@/views/canvasHelpers';
import { Endpoint } from 'jsplumb'; import { Endpoint } from 'jsplumb';
@@ -15,7 +14,6 @@ import { getStyleTokenValue } from '../helpers';
export const nodeBase = mixins( export const nodeBase = mixins(
deviceSupportHelpers, deviceSupportHelpers,
nodeIndex,
).extend({ ).extend({
mounted () { mounted () {
// Initialize the node // Initialize the node
@@ -28,10 +26,7 @@ export const nodeBase = mixins(
return this.$store.getters.getNodeByName(this.name); return this.$store.getters.getNodeByName(this.name);
}, },
nodeId (): string { nodeId (): string {
return NODE_NAME_PREFIX + this.nodeIndex; return this.data.id;
},
nodeIndex (): string {
return this.$store.getters.getNodeIndex(this.data.name).toString();
}, },
}, },
props: [ props: [
@@ -62,7 +57,7 @@ export const nodeBase = mixins(
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index]; const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
const newEndpointData: IEndpointOptions = { const newEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeIndex, index), uuid: CanvasHelpers.getInputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'Rectangle', endpoint: 'Rectangle',
@@ -71,7 +66,7 @@ export const nodeBase = mixins(
isSource: false, isSource: false,
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView, isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
parameters: { parameters: {
nodeIndex: this.nodeIndex, nodeId: this.nodeId,
type: inputName, type: inputName,
index, index,
}, },
@@ -130,7 +125,7 @@ export const nodeBase = mixins(
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index]; const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
const newEndpointData: IEndpointOptions = { const newEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index), uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'Dot', endpoint: 'Dot',
@@ -140,7 +135,7 @@ export const nodeBase = mixins(
isTarget: false, isTarget: false,
enabled: !this.isReadOnly, enabled: !this.isReadOnly,
parameters: { parameters: {
nodeIndex: this.nodeIndex, nodeId: this.nodeId,
type: inputName, type: inputName,
index, index,
}, },
@@ -166,7 +161,7 @@ export const nodeBase = mixins(
if (!this.isReadOnly) { if (!this.isReadOnly) {
const plusEndpointData: IEndpointOptions = { const plusEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index), uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'N8nPlus', endpoint: 'N8nPlus',
@@ -187,7 +182,7 @@ export const nodeBase = mixins(
hover: true, // hack to distinguish hover state hover: true, // hack to distinguish hover state
}, },
parameters: { parameters: {
nodeIndex: this.nodeIndex, nodeId: this.nodeId,
type: inputName, type: inputName,
index, index,
}, },
@@ -258,8 +253,7 @@ export const nodeBase = mixins(
// create a proper solution // create a proper solution
let newNodePositon: XYPosition; let newNodePositon: XYPosition;
moveNodes.forEach((node: INodeUi) => { moveNodes.forEach((node: INodeUi) => {
const nodeElement = `node-${this.getNodeIndex(node.name)}`; const element = document.getElementById(node.id);
const element = document.getElementById(nodeElement);
if (element === null) { if (element === null) {
return; return;
} }

View File

@@ -1,18 +0,0 @@
import Vue from 'vue';
export const nodeIndex = Vue.extend({
methods: {
getNodeIndex (nodeName: string): string {
let uniqueId = this.$store.getters.getNodeIndex(nodeName);
if (uniqueId === -1) {
this.$store.commit('addToNodeIndex', nodeName);
uniqueId = this.$store.getters.getNodeIndex(nodeName);
}
// We return as string as draggable and jsplumb seems to make problems
// when numbers are given
return uniqueId.toString();
},
},
});

View File

@@ -53,7 +53,7 @@ import { showMessage } from '@/components/mixins/showMessage';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuid } from 'uuid';
export const workflowHelpers = mixins( export const workflowHelpers = mixins(
externalHooks, externalHooks,
@@ -666,7 +666,7 @@ export const workflowHelpers = mixins(
} }
}, },
async saveAsNewWorkflow ({name, tags, resetWebhookUrls, openInNewWindow}: {name?: string, tags?: string[], resetWebhookUrls?: boolean, openInNewWindow?: boolean} = {}, redirect = true): Promise<boolean> { async saveAsNewWorkflow ({name, tags, resetWebhookUrls, resetNodeIds, openInNewWindow}: {name?: string, tags?: string[], resetWebhookUrls?: boolean, openInNewWindow?: boolean, resetNodeIds?: boolean} = {}, redirect = true): Promise<boolean> {
try { try {
this.$store.commit('addActiveAction', 'workflowSaving'); this.$store.commit('addActiveAction', 'workflowSaving');
@@ -674,10 +674,19 @@ export const workflowHelpers = mixins(
// make sure that the new ones are not active // make sure that the new ones are not active
workflowDataRequest.active = false; workflowDataRequest.active = false;
const changedNodes = {} as IDataObject; const changedNodes = {} as IDataObject;
if (resetNodeIds) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => {
node.id = uuid();
return node;
});
}
if (resetWebhookUrls) { if (resetWebhookUrls) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => { workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => {
if (node.webhookId) { if (node.webhookId) {
node.webhookId = uuidv4(); node.webhookId = uuid();
changedNodes[node.name] = node.webhookId; changedNodes[node.name] = node.webhookId;
} }
return node; return node;

View File

@@ -2,7 +2,6 @@ export const MAX_WORKFLOW_SIZE = 16777216; // Workflow size limit in bytes
export const MAX_WORKFLOW_PINNED_DATA_SIZE = 12582912; // Workflow pinned data size limit in bytes export const MAX_WORKFLOW_PINNED_DATA_SIZE = 12582912; // Workflow pinned data size limit in bytes
export const MAX_DISPLAY_DATA_SIZE = 204800; export const MAX_DISPLAY_DATA_SIZE = 204800;
export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250; export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250;
export const NODE_NAME_PREFIX = 'node-';
export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]'; export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]';

View File

@@ -79,7 +79,6 @@ const state: IRootState = {
workflowExecutionData: null, workflowExecutionData: null,
lastSelectedNode: null, lastSelectedNode: null,
lastSelectedNodeOutputIndex: null, lastSelectedNodeOutputIndex: null,
nodeIndex: [],
nodeViewOffsetPosition: [0, 0], nodeViewOffsetPosition: [0, 0],
nodeViewMoveInProgress: false, nodeViewMoveInProgress: false,
selectedNodes: [], selectedNodes: [],
@@ -533,17 +532,6 @@ export const store = new Vuex.Store({
Vue.set(state.nodeMetadata[node.name], 'parametersLastUpdatedAt', Date.now()); Vue.set(state.nodeMetadata[node.name], 'parametersLastUpdatedAt', Date.now());
}, },
// Node-Index
addToNodeIndex(state, nodeName: string) {
state.nodeIndex.push(nodeName);
},
setNodeIndex(state, newData: { index: number, name: string | null }) {
state.nodeIndex[newData.index] = newData.name;
},
resetNodeIndex(state) {
Vue.set(state, 'nodeIndex', []);
},
// Node-View // Node-View
setNodeViewMoveInProgress(state, value: boolean) { setNodeViewMoveInProgress(state, value: boolean) {
state.nodeViewMoveInProgress = value; state.nodeViewMoveInProgress = value;
@@ -821,14 +809,6 @@ export const store = new Vuex.Store({
}); });
}, },
// Node-Index
getNodeIndex: (state) => (nodeName: string): number => {
return state.nodeIndex.indexOf(nodeName);
},
getNodeNameByIndex: (state) => (index: number): string | null => {
return state.nodeIndex[index];
},
getNodeViewOffsetPosition: (state): XYPosition => { getNodeViewOffsetPosition: (state): XYPosition => {
return state.nodeViewOffsetPosition; return state.nodeViewOffsetPosition;
}, },
@@ -838,7 +818,16 @@ export const store = new Vuex.Store({
// Selected Nodes // Selected Nodes
getSelectedNodes: (state): INodeUi[] => { getSelectedNodes: (state): INodeUi[] => {
return state.selectedNodes; const seen = new Set();
return state.selectedNodes.filter((node: INodeUi) => {
// dedupe for instances when same node is selected in different ways
if (!seen.has(node.id)) {
seen.add(node.id);
return true;
}
return false;
});
}, },
isNodeSelected: (state) => (nodeName: string): boolean => { isNodeSelected: (state) => (nodeName: string): boolean => {
let index; let index;
@@ -874,6 +863,9 @@ export const store = new Vuex.Store({
getNodeByName: (state, getters) => (nodeName: string): INodeUi | null => { getNodeByName: (state, getters) => (nodeName: string): INodeUi | null => {
return getters.nodesByName[nodeName] || null; return getters.nodesByName[nodeName] || null;
}, },
getNodeById: (state, getters) => (nodeId: string): INodeUi | undefined => {
return state.workflow.nodes.find((node: INodeUi) => node.id === nodeId);
},
nodesIssuesExist: (state): boolean => { nodesIssuesExist: (state): boolean => {
for (const node of state.workflow.nodes) { for (const node of state.workflow.nodes) {
if (node.issues === undefined || Object.keys(node.issues).length === 0) { if (node.issues === undefined || Object.keys(node.issues).length === 0) {

View File

@@ -21,7 +21,7 @@
class="node-view" class="node-view"
:style="workflowStyle" :style="workflowStyle"
> >
<div v-for="nodeData in nodes" :key="getNodeIndex(nodeData.name)"> <div v-for="nodeData in nodes" :key="nodeData.id">
<node <node
v-if="nodeData.type !== STICKY_NODE_TYPE" v-if="nodeData.type !== STICKY_NODE_TYPE"
@duplicateNode="duplicateNode" @duplicateNode="duplicateNode"
@@ -32,8 +32,7 @@
@runWorkflow="onRunNode" @runWorkflow="onRunNode"
@moved="onNodeMoved" @moved="onNodeMoved"
@run="onNodeRun" @run="onNodeRun"
:id="'node-' + getNodeIndex(nodeData.name)" :key="nodeData.id"
:key="getNodeIndex(nodeData.name)"
:name="nodeData.name" :name="nodeData.name"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:instance="instance" :instance="instance"
@@ -46,7 +45,7 @@
@deselectNode="nodeDeselectedByName" @deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName" @nodeSelected="nodeSelectedByName"
@removeNode="removeNode" @removeNode="removeNode"
:id="'node-' + getNodeIndex(nodeData.name)" :key="nodeData.id"
:name="nodeData.name" :name="nodeData.name"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:instance="instance" :instance="instance"
@@ -161,7 +160,6 @@ import {
MODAL_CANCEL, MODAL_CANCEL,
MODAL_CLOSE, MODAL_CLOSE,
MODAL_CONFIRMED, MODAL_CONFIRMED,
NODE_NAME_PREFIX,
NODE_OUTPUT_DEFAULT_KEY, NODE_OUTPUT_DEFAULT_KEY,
ONBOARDING_CALL_SIGNUP_MODAL_KEY, ONBOARDING_CALL_SIGNUP_MODAL_KEY,
ONBOARDING_PROMPT_TIMEBOX, ONBOARDING_PROMPT_TIMEBOX,
@@ -195,7 +193,7 @@ import Sticky from '@/components/Sticky.vue';
import * as CanvasHelpers from './canvasHelpers'; import * as CanvasHelpers from './canvasHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { v4 as uuidv4} from 'uuid'; import { v4 as uuid } from 'uuid';
import { import {
IConnection, IConnection,
IConnections, IConnections,
@@ -228,6 +226,7 @@ import {
ITag, ITag,
IWorkflowTemplate, IWorkflowTemplate,
IExecutionsSummary, IExecutionsSummary,
IWorkflowToShare,
} from '../Interface'; } from '../Interface';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -639,6 +638,7 @@ export default mixins(
} }
this.resetWorkspace(); this.resetWorkspace();
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes); data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
await this.addNodes(data.workflow.nodes, data.workflow.connections); await this.addNodes(data.workflow.nodes, data.workflow.connections);
if (data.workflow.pinData) { if (data.workflow.pinData) {
@@ -1094,7 +1094,14 @@ export default mixins(
copySelectedNodes (isCut: boolean) { copySelectedNodes (isCut: boolean) {
this.getSelectedNodesToSave().then((data) => { this.getSelectedNodesToSave().then((data) => {
const nodeData = JSON.stringify(data, null, 2); const workflowToCopy: IWorkflowToShare = {
meta: {
instanceId: this.$store.getters.instanceId,
},
...data,
};
const nodeData = JSON.stringify(workflowToCopy, null, 2);
this.copyToClipboard(nodeData); this.copyToClipboard(nodeData);
if (data.nodes.length > 0) { if (data.nodes.length > 0) {
if(!isCut){ if(!isCut){
@@ -1290,11 +1297,7 @@ export default mixins(
} }
} }
this.$telemetry.track('User pasted nodes', { return this.importWorkflowData(workflowData!, false, 'paste');
workflow_id: this.$store.getters.workflowId,
});
return this.importWorkflowData(workflowData!, false);
}, },
// Returns the workflow data from a given URL. If no data gets found or // Returns the workflow data from a given URL. If no data gets found or
@@ -1315,13 +1318,11 @@ export default mixins(
} }
this.stopLoading(); this.stopLoading();
this.$telemetry.track('User imported workflow', { source: 'url', workflow_id: this.$store.getters.workflowId });
return workflowData; return workflowData;
}, },
// Imports the given workflow data into the current workflow // Imports the given workflow data into the current workflow
async importWorkflowData (workflowData: IWorkflowDataUpdate, importTags = true): Promise<void> { async importWorkflowData (workflowData: IWorkflowToShare, importTags = true, source: string): Promise<void> {
// If it is JSON check if it looks on the first look like data we can use // If it is JSON check if it looks on the first look like data we can use
if ( if (
!workflowData.hasOwnProperty('nodes') || !workflowData.hasOwnProperty('nodes') ||
@@ -1331,6 +1332,40 @@ export default mixins(
} }
try { try {
const nodeIdMap: {[prev: string]: string} = {};
if (workflowData.nodes) {
// set all new ids when pasting/importing workflows
workflowData.nodes.forEach((node: INode) => {
if (node.id) {
const newId = uuid();
nodeIdMap[newId] = node.id;
node.id = newId;
}
else {
node.id = uuid();
}
});
}
const currInstanceId = this.$store.getters.instanceId;
const nodeGraph = JSON.stringify(
TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase,
this.getNodeTypes(),
{
nodeIdMap,
sourceInstanceId: workflowData.meta && workflowData.meta.instanceId !== currInstanceId? workflowData.meta.instanceId: '',
}).nodeGraph,
);
if (source === 'paste') {
this.$telemetry.track('User pasted nodes', {
workflow_id: this.$store.getters.workflowId,
node_graph_string: nodeGraph,
});
} else {
this.$telemetry.track('User imported workflow', { source, workflow_id: this.$store.getters.workflowId, node_graph_string: nodeGraph });
}
// By default we automatically deselect all the currently // By default we automatically deselect all the currently
// selected nodes and select the new ones // selected nodes and select the new ones
this.deselectAllNodes(); this.deselectAllNodes();
@@ -1500,6 +1535,7 @@ export default mixins(
} }
const newNodeData: INodeUi = { const newNodeData: INodeUi = {
id: uuid(),
name: nodeTypeData.defaults.name as string, name: nodeTypeData.defaults.name as string,
type: nodeTypeData.name, type: nodeTypeData.name,
typeVersion: Array.isArray(nodeTypeData.version) typeVersion: Array.isArray(nodeTypeData.version)
@@ -1564,7 +1600,7 @@ export default mixins(
}); });
if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) {
newNodeData.webhookId = uuidv4(); newNodeData.webhookId = uuid();
} }
await this.addNodes([newNodeData]); await this.addNodes([newNodeData]);
@@ -1676,8 +1712,12 @@ export default mixins(
// Get the node and set it as active that new nodes // Get the node and set it as active that new nodes
// which get created get automatically connected // which get created get automatically connected
// to it. // to it.
const sourceNodeName = this.$store.getters.getNodeNameByIndex(info.sourceId.slice(NODE_NAME_PREFIX.length)); const sourceNode = this.$store.getters.getNodeById(info.sourceId) as INodeUi | null;
this.$store.commit('setLastSelectedNode', sourceNodeName); if (!sourceNode) {
return;
}
this.$store.commit('setLastSelectedNode', sourceNode.name);
this.$store.commit('setLastSelectedNodeOutputIndex', info.index); this.$store.commit('setLastSelectedNodeOutputIndex', info.index);
this.newNodeInsertPosition = null; this.newNodeInsertPosition = null;
@@ -1696,7 +1736,8 @@ export default mixins(
} }
if (this.pullConnActiveNodeName) { if (this.pullConnActiveNodeName) {
const sourceNodeName = this.$store.getters.getNodeNameByIndex(connection.sourceId.slice(NODE_NAME_PREFIX.length)); const sourceNode = this.$store.getters.getNodeById(connection.sourceId);
const sourceNodeName = sourceNode.name;
const outputIndex = connection.getParameters().index; const outputIndex = connection.getParameters().index;
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0); this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
@@ -1720,8 +1761,8 @@ export default mixins(
// @ts-ignore // @ts-ignore
const targetInfo = info.dropEndpoint.getParameters(); const targetInfo = info.dropEndpoint.getParameters();
const sourceNodeName = this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex); const sourceNodeName = this.$store.getters.getNodeById(sourceInfo.nodeId).name;
const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex); const targetNodeName = this.$store.getters.getNodeById(targetInfo.nodeId).name;
// check for duplicates // check for duplicates
if (this.getConnection(sourceNodeName, sourceInfo.index, targetNodeName, targetInfo.index)) { if (this.getConnection(sourceNodeName, sourceInfo.index, targetNodeName, targetInfo.index)) {
@@ -1745,8 +1786,8 @@ export default mixins(
const sourceInfo = info.sourceEndpoint.getParameters(); const sourceInfo = info.sourceEndpoint.getParameters();
const targetInfo = info.targetEndpoint.getParameters(); const targetInfo = info.targetEndpoint.getParameters();
const sourceNodeName = this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex); const sourceNodeName = this.$store.getters.getNodeById(sourceInfo.nodeId).name;
const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex); const targetNodeName = this.$store.getters.getNodeById(targetInfo.nodeId).name;
info.connection.__meta = { info.connection.__meta = {
sourceNodeName, sourceNodeName,
@@ -1872,12 +1913,12 @@ export default mixins(
const connectionInfo = [ const connectionInfo = [
{ {
node: this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex), node: this.$store.getters.getNodeById(sourceInfo.nodeId).name,
type: sourceInfo.type, type: sourceInfo.type,
index: sourceInfo.index, index: sourceInfo.index,
}, },
{ {
node: this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex), node: this.$store.getters.getNodeById(targetInfo.nodeId).name,
type: targetInfo.type, type: targetInfo.type,
index: targetInfo.index, index: targetInfo.index,
}, },
@@ -1896,7 +1937,8 @@ export default mixins(
this.__removeConnectionByConnectionInfo(info, false); this.__removeConnectionByConnectionInfo(info, false);
if (this.pullConnActiveNodeName) { // establish new connection when dragging connection from one node to another if (this.pullConnActiveNodeName) { // establish new connection when dragging connection from one node to another
const sourceNodeName = this.$store.getters.getNodeNameByIndex(info.connection.sourceId.slice(NODE_NAME_PREFIX.length)); const sourceNode = this.$store.getters.getNodeById(info.connection.sourceId);
const sourceNodeName = sourceNode.name;
const outputIndex = info.connection.getParameters().index; const outputIndex = info.connection.getParameters().index;
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0); this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
@@ -1940,11 +1982,14 @@ export default mixins(
const nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null; const nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null;
if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) { if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) {
this.pullConnActiveNodeName = node.name; this.pullConnActiveNodeName = node.name;
const endpoint = this.instance.getEndpoint(this.getInputEndpointUUID(nodeName, 0)); const endpointUUID = this.getInputEndpointUUID(nodeName, 0);
if (endpointUUID) {
const endpoint = this.instance.getEndpoint(endpointUUID);
CanvasHelpers.showDropConnectionState(connection, endpoint); CanvasHelpers.showDropConnectionState(connection, endpoint);
return true; return true;
}
} }
} }
} }
@@ -1992,7 +2037,10 @@ export default mixins(
this.$store.commit('setStateDirty', false); this.$store.commit('setStateDirty', false);
await this.addNodes([{...CanvasHelpers.DEFAULT_START_NODE}]); await this.addNodes([{
id: uuid(),
...CanvasHelpers.DEFAULT_START_NODE,
}]);
this.nodeSelectedByName(CanvasHelpers.DEFAULT_START_NODE.name, false); this.nodeSelectedByName(CanvasHelpers.DEFAULT_START_NODE.name, false);
@@ -2007,6 +2055,7 @@ export default mixins(
this.$nextTick(async () => { this.$nextTick(async () => {
await this.addNodes([ await this.addNodes([
{ {
id: uuid(),
...CanvasHelpers.WELCOME_STICKY_NODE, ...CanvasHelpers.WELCOME_STICKY_NODE,
parameters: { parameters: {
// Use parameters from the template but add translated content // Use parameters from the template but add translated content
@@ -2108,17 +2157,33 @@ export default mixins(
} }
}); });
}, },
getOutputEndpointUUID(nodeName: string, index: number) { getOutputEndpointUUID(nodeName: string, index: number): string | null {
return CanvasHelpers.getOutputEndpointUUID(this.getNodeIndex(nodeName), index); const node = this.$store.getters.getNodeByName(nodeName);
if (!node) {
return null;
}
return CanvasHelpers.getOutputEndpointUUID(node.id, index);
}, },
getInputEndpointUUID(nodeName: string, index: number) { getInputEndpointUUID(nodeName: string, index: number) {
return CanvasHelpers.getInputEndpointUUID(this.getNodeIndex(nodeName), index); const node = this.$store.getters.getNodeByName(nodeName);
if (!node) {
return null;
}
return CanvasHelpers.getInputEndpointUUID(node.id, index);
}, },
__addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) {
if (addVisualConnection === true) { if (addVisualConnection === true) {
const outputUuid = this.getOutputEndpointUUID(connection[0].node, connection[0].index);
const inputUuid = this.getInputEndpointUUID(connection[1].node, connection[1].index);
if (!outputUuid || !inputUuid) {
return;
}
const uuid: [string, string] = [ const uuid: [string, string] = [
this.getOutputEndpointUUID(connection[0].node, connection[0].index), outputUuid,
this.getInputEndpointUUID(connection[1].node, connection[1].index), inputUuid,
]; ];
// Create connections in DOM // Create connections in DOM
@@ -2140,10 +2205,12 @@ export default mixins(
}, },
__removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) { __removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) {
if (removeVisualConnection === true) { if (removeVisualConnection === true) {
const sourceId = this.$store.getters.getNodeByName(connection[0].node);
const targetId = this.$store.getters.getNodeByName(connection[1].node);
// @ts-ignore // @ts-ignore
const connections = this.instance.getConnections({ const connections = this.instance.getConnections({
source: NODE_NAME_PREFIX + this.getNodeIndex(connection[0].node), source: sourceId,
target: NODE_NAME_PREFIX + this.getNodeIndex(connection[1].node), target: targetId,
}); });
// @ts-ignore // @ts-ignore
@@ -2175,12 +2242,12 @@ export default mixins(
const connectionInfo = [ const connectionInfo = [
{ {
node: this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex), node: this.$store.getters.getNodeById(sourceInfo.nodeId).name,
type: sourceInfo.type, type: sourceInfo.type,
index: sourceInfo.index, index: sourceInfo.index,
}, },
{ {
node: this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex), node: this.$store.getters.getNodeById(targetInfo.nodeId).name,
type: targetInfo.type, type: targetInfo.type,
index: targetInfo.index, index: targetInfo.index,
}, },
@@ -2208,6 +2275,7 @@ export default mixins(
// Deep copy the data so that data on lower levels of the node-properties do // Deep copy the data so that data on lower levels of the node-properties do
// not share objects // not share objects
const newNodeData = JSON.parse(JSON.stringify(this.getNodeDataToSave(node))); const newNodeData = JSON.parse(JSON.stringify(this.getNodeDataToSave(node)));
newNodeData.id = uuid();
// Check if node-name is unique else find one that is // Check if node-name is unique else find one that is
newNodeData.name = this.getUniqueNodeName({ newNodeData.name = this.getUniqueNodeName({
@@ -2223,7 +2291,7 @@ export default mixins(
if (newNodeData.webhookId) { if (newNodeData.webhookId) {
// Make sure that the node gets a new unique webhook-ID // Make sure that the node gets a new unique webhook-ID
newNodeData.webhookId = uuidv4(); newNodeData.webhookId = uuid();
} }
await this.addNodes([newNodeData]); await this.addNodes([newNodeData]);
@@ -2248,14 +2316,17 @@ export default mixins(
this.$telemetry.track('User duplicated node', { node_type: node.type, workflow_id: this.$store.getters.workflowId }); this.$telemetry.track('User duplicated node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
}, },
getJSPlumbConnection (sourceNodeName: string, sourceOutputIndex: number, targetNodeName: string, targetInputIndex: number): Connection | undefined { getJSPlumbConnection (sourceNodeName: string, sourceOutputIndex: number, targetNodeName: string, targetInputIndex: number): Connection | undefined {
const sourceIndex = this.getNodeIndex(sourceNodeName); const sourceNode = this.$store.getters.getNodeByName(sourceNodeName) as INodeUi;
const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`; const targetNode = this.$store.getters.getNodeByName(targetNodeName) as INodeUi;
if (!sourceNode || !targetNode) {
return;
}
const targetIndex = this.getNodeIndex(targetNodeName); const sourceId = sourceNode.id;
const targetId = `${NODE_NAME_PREFIX}${targetIndex}`; const targetId = targetNode.id;
const sourceEndpoint = CanvasHelpers.getOutputEndpointUUID(sourceIndex, sourceOutputIndex); const sourceEndpoint = CanvasHelpers.getOutputEndpointUUID(sourceId, sourceOutputIndex);
const targetEndpoint = CanvasHelpers.getInputEndpointUUID(targetIndex, targetInputIndex); const targetEndpoint = CanvasHelpers.getInputEndpointUUID(targetId, targetInputIndex);
// @ts-ignore // @ts-ignore
const connections = this.instance.getConnections({ const connections = this.instance.getConnections({
@@ -2269,9 +2340,8 @@ export default mixins(
}); });
}, },
getJSPlumbEndpoints (nodeName: string): Endpoint[] { getJSPlumbEndpoints (nodeName: string): Endpoint[] {
const nodeIndex = this.getNodeIndex(nodeName); const node = this.$store.getters.getNodeByName(nodeName);
const nodeId = `${NODE_NAME_PREFIX}${nodeIndex}`; return this.instance.getEndpoints(node.id);
return this.instance.getEndpoints(nodeId);
}, },
getPlusEndpoint (nodeName: string, outputIndex: number): Endpoint | undefined { getPlusEndpoint (nodeName: string, outputIndex: number): Endpoint | undefined {
const endpoints = this.getJSPlumbEndpoints(nodeName); const endpoints = this.getJSPlumbEndpoints(nodeName);
@@ -2279,15 +2349,15 @@ export default mixins(
return endpoints.find((endpoint: Endpoint) => endpoint.type === 'N8nPlus' && endpoint.__meta && endpoint.__meta.index === outputIndex); return endpoints.find((endpoint: Endpoint) => endpoint.type === 'N8nPlus' && endpoint.__meta && endpoint.__meta.index === outputIndex);
}, },
getIncomingOutgoingConnections(nodeName: string): {incoming: Connection[], outgoing: Connection[]} { getIncomingOutgoingConnections(nodeName: string): {incoming: Connection[], outgoing: Connection[]} {
const name = `${NODE_NAME_PREFIX}${this.$store.getters.getNodeIndex(nodeName)}`; const node = this.$store.getters.getNodeByName(nodeName);
// @ts-ignore // @ts-ignore
const outgoing = this.instance.getConnections({ const outgoing = this.instance.getConnections({
source: name, source: node.id,
}) as Connection[]; }) as Connection[];
// @ts-ignore // @ts-ignore
const incoming = this.instance.getConnections({ const incoming = this.instance.getConnections({
target: name, target: node.id,
}) as Connection[]; }) as Connection[];
return { return {
@@ -2305,8 +2375,8 @@ export default mixins(
}, },
onNodeRun ({name, data, waiting}: {name: string, data: ITaskData[] | null, waiting: boolean}) { onNodeRun ({name, data, waiting}: {name: string, data: ITaskData[] | null, waiting: boolean}) {
const sourceNodeName = name; const sourceNodeName = name;
const sourceIndex = this.$store.getters.getNodeIndex(sourceNodeName); const sourceNode = this.$store.getters.getNodeByName(sourceNodeName);
const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`; const sourceId = sourceNode.id;
if (data === null || data.length === 0 || waiting) { if (data === null || data.length === 0 || waiting) {
// @ts-ignore // @ts-ignore
@@ -2438,18 +2508,15 @@ export default mixins(
} }
setTimeout(() => { setTimeout(() => {
const nodeIndex = this.$store.getters.getNodeIndex(nodeName);
const nodeIdName = `node-${nodeIndex}`;
// Suspend drawing // Suspend drawing
this.instance.setSuspendDrawing(true); this.instance.setSuspendDrawing(true);
// Remove all endpoints and the connections in jsplumb // Remove all endpoints and the connections in jsplumb
this.instance.removeAllEndpoints(nodeIdName); this.instance.removeAllEndpoints(node.id);
// Remove the draggable // Remove the draggable
// @ts-ignore // @ts-ignore
this.instance.destroyDraggable(nodeIdName); this.instance.destroyDraggable(node.id);
// Remove the connections in data // Remove the connections in data
this.$store.commit('removeAllNodeConnection', node); this.$store.commit('removeAllNodeConnection', node);
@@ -2465,10 +2532,6 @@ export default mixins(
// Remove node from selected index if found in it // Remove node from selected index if found in it
this.$store.commit('removeNodeFromSelection', node); this.$store.commit('removeNodeFromSelection', node);
// Remove from node index
if (nodeIndex !== -1) {
this.$store.commit('setNodeIndex', { index: nodeIndex, name: null });
}
}, 0); // allow other events to finish like drag stop }, 0); // allow other events to finish like drag stop
}, },
valueChanged (parameterData: IUpdateInformation) { valueChanged (parameterData: IUpdateInformation) {
@@ -2557,7 +2620,7 @@ export default mixins(
try { try {
const nodes = this.$store.getters.allNodes as INodeUi[]; const nodes = this.$store.getters.allNodes as INodeUi[];
// @ts-ignore // @ts-ignore
nodes.forEach((node: INodeUi) => this.instance.destroyDraggable(`${NODE_NAME_PREFIX}${this.$store.getters.getNodeIndex(node.name)}`)); nodes.forEach((node: INodeUi) => this.instance.destroyDraggable(node.id));
this.instance.deleteEveryEndpoint(); this.instance.deleteEveryEndpoint();
} catch (e) {} } catch (e) {}
@@ -2621,6 +2684,10 @@ export default mixins(
let nodeType: INodeTypeDescription | null; let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null; let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => { nodes.forEach((node) => {
if (!node.id) {
node.id = uuid();
}
nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null; nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null;
// Make sure that some properties always exist // Make sure that some properties always exist
@@ -2919,7 +2986,6 @@ export default mixins(
this.$store.commit('removeActiveAction', 'workflowRunning'); this.$store.commit('removeActiveAction', 'workflowRunning');
this.$store.commit('setExecutionWaitingForWebhook', false); this.$store.commit('setExecutionWaitingForWebhook', false);
this.$store.commit('resetNodeIndex');
this.$store.commit('resetSelectedNodes'); this.$store.commit('resetSelectedNodes');
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false}); this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false});
@@ -2983,19 +3049,24 @@ export default mixins(
} }
}, },
async onImportWorkflowDataEvent(data: IDataObject) { async onImportWorkflowDataEvent(data: IDataObject) {
await this.importWorkflowData(data.data as IWorkflowDataUpdate); await this.importWorkflowData(data.data as IWorkflowDataUpdate, undefined, 'file');
}, },
async onImportWorkflowUrlEvent(data: IDataObject) { async onImportWorkflowUrlEvent(data: IDataObject) {
const workflowData = await this.getWorkflowDataFromUrl(data.url as string); const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
if (workflowData !== undefined) { if (workflowData !== undefined) {
await this.importWorkflowData(workflowData); await this.importWorkflowData(workflowData, undefined, 'url');
} }
}, },
addPinDataConnections(pinData: IPinData) { addPinDataConnections(pinData: IPinData) {
Object.keys(pinData).forEach((nodeName) => { Object.keys(pinData).forEach((nodeName) => {
const node = this.$store.getters.getNodeByName(nodeName);
if (!node) {
return;
}
// @ts-ignore // @ts-ignore
const connections = this.instance.getConnections({ const connections = this.instance.getConnections({
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName), source: node.id,
}) as Connection[]; }) as Connection[];
connections.forEach((connection) => { connections.forEach((connection) => {
@@ -3008,9 +3079,14 @@ export default mixins(
}, },
removePinDataConnections(pinData: IPinData) { removePinDataConnections(pinData: IPinData) {
Object.keys(pinData).forEach((nodeName) => { Object.keys(pinData).forEach((nodeName) => {
const node = this.$store.getters.getNodeByName(nodeName);
if (!node) {
return;
}
// @ts-ignore // @ts-ignore
const connections = this.instance.getConnections({ const connections = this.instance.getConnections({
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName), source: node.id,
}) as Connection[]; }) as Connection[];
connections.forEach(CanvasHelpers.resetConnection); connections.forEach(CanvasHelpers.resetConnection);

View File

@@ -10,6 +10,7 @@ import {
NodeInputConnections, NodeInputConnections,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
export const OVERLAY_DROP_NODE_ID = 'drop-add-node'; export const OVERLAY_DROP_NODE_ID = 'drop-add-node';
export const OVERLAY_MIDPOINT_ARROW_ID = 'midpoint-arrow'; export const OVERLAY_MIDPOINT_ARROW_ID = 'midpoint-arrow';
@@ -705,12 +706,12 @@ export const addConnectionActionsOverlay = (connection: Connection, onDelete: Fu
]); ]);
}; };
export const getOutputEndpointUUID = (nodeIndex: string, outputIndex: number) => { export const getOutputEndpointUUID = (nodeId: string, outputIndex: number) => {
return `${nodeIndex}${OUTPUT_UUID_KEY}${outputIndex}`; return `${nodeId}${OUTPUT_UUID_KEY}${outputIndex}`;
}; };
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => { export const getInputEndpointUUID = (nodeId: string, inputIndex: number) => {
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`; return `${nodeId}${INPUT_UUID_KEY}${inputIndex}`;
}; };
export const getFixedNodesList = (workflowNodes: INode[]) => { export const getFixedNodesList = (workflowNodes: INode[]) => {
@@ -728,7 +729,7 @@ export const getFixedNodesList = (workflowNodes: INode[]) => {
}); });
if (!hasStartNode) { if (!hasStartNode) {
nodes.push({...DEFAULT_START_NODE}); nodes.push({...DEFAULT_START_NODE, id: uuid() });
} }
return nodes; return nodes;
}; };

View File

@@ -823,6 +823,7 @@ export interface INodeCredentials {
} }
export interface INode { export interface INode {
id: string;
name: string; name: string;
typeVersion: number; typeVersion: number;
type: string; type: string;
@@ -1542,6 +1543,7 @@ export interface INoteGraphItem {
} }
export interface INodeGraphItem { export interface INodeGraphItem {
id: string;
type: string; type: string;
resource?: string; resource?: string;
operation?: string; operation?: string;
@@ -1553,6 +1555,8 @@ export interface INodeGraphItem {
credential_type?: string; // HTTP Request node v2 credential_type?: string; // HTTP Request node v2
credential_set?: boolean; // HTTP Request node v2 credential_set?: boolean; // HTTP Request node v2
method?: string; // HTTP Request node v2 method?: string; // HTTP Request node v2
src_node_id?: string;
src_instance_id?: string;
} }
export interface INodeNameIndex { export interface INodeNameIndex {

View File

@@ -22,10 +22,10 @@ export function isNumber(value: unknown): value is number {
} }
function getStickyDimensions(note: INode, stickyType: INodeType | undefined) { function getStickyDimensions(note: INode, stickyType: INodeType | undefined) {
const heightProperty = stickyType?.description.properties.find( const heightProperty = stickyType?.description?.properties.find(
(property) => property.name === 'height', (property) => property.name === 'height',
); );
const widthProperty = stickyType?.description.properties.find( const widthProperty = stickyType?.description?.properties.find(
(property) => property.name === 'width', (property) => property.name === 'width',
); );
@@ -114,6 +114,10 @@ export function getDomainPath(raw: string, urlParts = URL_PARTS_REGEX): string {
export function generateNodesGraph( export function generateNodesGraph(
workflow: IWorkflowBase, workflow: IWorkflowBase,
nodeTypes: INodeTypes, nodeTypes: INodeTypes,
options?: {
sourceInstanceId?: string;
nodeIdMap?: { [curr: string]: string };
},
): INodesGraphResult { ): INodesGraphResult {
const nodesGraph: INodesGraph = { const nodesGraph: INodesGraph = {
node_types: [], node_types: [],
@@ -149,10 +153,19 @@ export function generateNodesGraph(
otherNodes.forEach((node: INode, index: number) => { otherNodes.forEach((node: INode, index: number) => {
nodesGraph.node_types.push(node.type); nodesGraph.node_types.push(node.type);
const nodeItem: INodeGraphItem = { const nodeItem: INodeGraphItem = {
id: node.id,
type: node.type, type: node.type,
position: node.position, position: node.position,
}; };
if (options?.sourceInstanceId) {
nodeItem.src_instance_id = options.sourceInstanceId;
}
if (node.id && options?.nodeIdMap && options.nodeIdMap[node.id]) {
nodeItem.src_node_id = options.nodeIdMap[node.id];
}
if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) { if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) {
try { try {
nodeItem.domain = new URL(node.parameters.url as string).hostname; nodeItem.domain = new URL(node.parameters.url as string).hostname;
@@ -182,7 +195,7 @@ export function generateNodesGraph(
} else { } else {
const nodeType = nodeTypes.getByNameAndVersion(node.type); const nodeType = nodeTypes.getByNameAndVersion(node.type);
nodeType?.description.properties.forEach((property) => { nodeType?.description?.properties?.forEach((property) => {
if ( if (
property.name === 'operation' || property.name === 'operation' ||
property.name === 'resource' || property.name === 'resource' ||
@@ -212,7 +225,7 @@ export function generateNodesGraph(
}); });
}); });
}); });
} catch (_) { } catch (e) {
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames }; return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
} }

View File

@@ -21,6 +21,7 @@ describe('Expression', () => {
name: 'node', name: 'node',
typeVersion: 1, typeVersion: 1,
type: 'test.set', type: 'test.set',
id: 'uuid-1234',
position: [0, 0], position: [0, 0],
parameters: {} parameters: {}
} }

View File

@@ -613,6 +613,7 @@ describe('RoutingNode', () => {
name: 'test', name: 'test',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1234',
position: [0, 0], position: [0, 0],
}; };
@@ -1659,6 +1660,7 @@ describe('RoutingNode', () => {
name: 'test', name: 'test',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1234',
position: [0, 0], position: [0, 0],
}; };
@@ -1831,6 +1833,7 @@ describe('RoutingNode', () => {
name: 'test', name: 'test',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1234',
position: [0, 0], position: [0, 0],
}; };

View File

@@ -548,6 +548,7 @@ describe('Workflow', () => {
parameters: stubData.parameters, parameters: stubData.parameters,
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1234',
position: [100, 100], position: [100, 100],
}; };
} }
@@ -1008,6 +1009,7 @@ describe('Workflow', () => {
parameters: testData.input.Node1.parameters, parameters: testData.input.Node1.parameters,
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [100, 100], position: [100, 100],
}, },
{ {
@@ -1015,6 +1017,7 @@ describe('Workflow', () => {
parameters: testData.input.Node2.parameters, parameters: testData.input.Node2.parameters,
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [100, 200], position: [100, 200],
}, },
{ {
@@ -1026,6 +1029,7 @@ describe('Workflow', () => {
: {}, : {},
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [100, 300], position: [100, 300],
}, },
{ {
@@ -1037,6 +1041,7 @@ describe('Workflow', () => {
: {}, : {},
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-4',
position: [100, 400], position: [100, 400],
}, },
]; ];
@@ -1219,6 +1224,7 @@ describe('Workflow', () => {
}, },
type: 'test.setMulti', type: 'test.setMulti',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1234',
position: [100, 100], position: [100, 100],
}, },
]; ];
@@ -1296,6 +1302,7 @@ describe('Workflow', () => {
name: 'Start', name: 'Start',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [240, 300], position: [240, 300],
}, },
{ {
@@ -1305,6 +1312,7 @@ describe('Workflow', () => {
name: 'Set', name: 'Set',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [460, 300], position: [460, 300],
}, },
{ {
@@ -1314,6 +1322,7 @@ describe('Workflow', () => {
name: 'Set1', name: 'Set1',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [680, 300], position: [680, 300],
}, },
], ],
@@ -1353,6 +1362,7 @@ describe('Workflow', () => {
name: 'Switch', name: 'Switch',
type: 'test.switch', type: 'test.switch',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [460, 300], position: [460, 300],
}, },
{ {
@@ -1362,6 +1372,7 @@ describe('Workflow', () => {
name: 'Set', name: 'Set',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [740, 300], position: [740, 300],
}, },
{ {
@@ -1371,6 +1382,7 @@ describe('Workflow', () => {
name: 'Set1', name: 'Set1',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [780, 100], position: [780, 100],
}, },
{ {
@@ -1380,6 +1392,7 @@ describe('Workflow', () => {
name: 'Set2', name: 'Set2',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-4',
position: [1040, 260], position: [1040, 260],
}, },
], ],
@@ -1443,6 +1456,7 @@ describe('Workflow', () => {
name: 'Switch', name: 'Switch',
type: 'test.switch', type: 'test.switch',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [920, 340], position: [920, 340],
}, },
{ {
@@ -1450,6 +1464,7 @@ describe('Workflow', () => {
name: 'Start', name: 'Start',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [240, 300], position: [240, 300],
}, },
{ {
@@ -1459,6 +1474,7 @@ describe('Workflow', () => {
name: 'Set1', name: 'Set1',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [700, 340], position: [700, 340],
}, },
{ {
@@ -1468,6 +1484,7 @@ describe('Workflow', () => {
name: 'Set', name: 'Set',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-4',
position: [1220, 300], position: [1220, 300],
}, },
{ {
@@ -1475,6 +1492,7 @@ describe('Workflow', () => {
name: 'Switch', name: 'Switch',
type: 'test.switch', type: 'test.switch',
typeVersion: 1, typeVersion: 1,
id: 'uuid-5',
position: [920, 340], position: [920, 340],
}, },
], ],

View File

@@ -10,6 +10,7 @@ describe('WorkflowDataProxy', () => {
name: 'Start', name: 'Start',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-1',
position: [100, 200], position: [100, 200],
}, },
{ {
@@ -20,6 +21,7 @@ describe('WorkflowDataProxy', () => {
name: 'Function', name: 'Function',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-2',
position: [280, 200], position: [280, 200],
}, },
{ {
@@ -36,6 +38,7 @@ describe('WorkflowDataProxy', () => {
name: 'Rename', name: 'Rename',
type: 'test.set', type: 'test.set',
typeVersion: 1, typeVersion: 1,
id: 'uuid-3',
position: [460, 200], position: [460, 200],
}, },
]; ];