mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
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:
parent
b5ea666ecf
commit
679a443a0c
|
@ -17,6 +17,7 @@ import fs from 'fs';
|
|||
import glob from 'fast-glob';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { EntityManager, getConnection } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getLogger } from '../../src/Logger';
|
||||
import { Db, ICredentialsDb, IWorkflowToImport } from '../../src';
|
||||
import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow';
|
||||
|
@ -129,6 +130,11 @@ export class ImportWorkflowsCommand extends Command {
|
|||
if (credentials.length > 0) {
|
||||
workflow.nodes.forEach((node: INode) => {
|
||||
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) {
|
||||
workflow.nodes.forEach((node: INode) => {
|
||||
this.transformCredentials(node, credentials);
|
||||
|
||||
if (!node.id) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
node.id = uuid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -586,6 +586,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
}
|
||||
|
||||
const node: INode = {
|
||||
id: 'temp',
|
||||
parameters: {},
|
||||
name: 'Temp-Node',
|
||||
type: nodeType.description.name,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: 0f5532f9-36ba-4bef-86c7-30d607400b15
|
||||
name:
|
||||
type: string
|
||||
example: Jira
|
||||
|
|
|
@ -7,7 +7,7 @@ import config = require('../../../../../config');
|
|||
import { WorkflowEntity } from '../../../../databases/entities/WorkflowEntity';
|
||||
import { InternalHooksManager } from '../../../../InternalHooksManager';
|
||||
import { externalHooks } from '../../../../Server';
|
||||
import { replaceInvalidCredentials } from '../../../../WorkflowHelpers';
|
||||
import { addNodeIds, replaceInvalidCredentials } from '../../../../WorkflowHelpers';
|
||||
import { WorkflowRequest } from '../../../types';
|
||||
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||
|
@ -42,6 +42,8 @@ export = {
|
|||
|
||||
await replaceInvalidCredentials(workflow);
|
||||
|
||||
addNodeIds(workflow);
|
||||
|
||||
const role = await getWorkflowOwnerRole();
|
||||
|
||||
const createdWorkflow = await createWorkflow(workflow, req.user, role);
|
||||
|
@ -186,6 +188,7 @@ export = {
|
|||
}
|
||||
|
||||
await replaceInvalidCredentials(updateData);
|
||||
addNodeIds(updateData);
|
||||
|
||||
const workflowRunner = ActiveWorkflowRunner.getInstance();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FindManyOptions, In, UpdateResult } from 'typeorm';
|
||||
import intersection from 'lodash.intersection';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Db } from '../../../..';
|
||||
import { User } from '../../../../databases/entities/User';
|
||||
|
@ -133,6 +134,7 @@ export function hasStartNode(workflow: WorkflowEntity): boolean {
|
|||
|
||||
export function getStartNode(): INode {
|
||||
return {
|
||||
id: uuid(),
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
|
|
@ -924,6 +924,8 @@ class App {
|
|||
// check credentials for old format
|
||||
await WorkflowHelpers.replaceInvalidCredentials(updateData);
|
||||
|
||||
WorkflowHelpers.addNodeIds(updateData);
|
||||
|
||||
await this.externalHooks.run('workflow.update', [updateData]);
|
||||
|
||||
if (shared.workflow.active) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
LoggerProxy as Logger,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
CredentialTypes,
|
||||
|
@ -474,6 +475,22 @@ export async function getStaticDataById(workflowId: string | number) {
|
|||
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
|
||||
export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise<WorkflowEntity> {
|
||||
const { nodes } = workflow;
|
||||
|
|
|
@ -43,6 +43,8 @@ workflowsController.post(
|
|||
|
||||
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||
|
||||
WorkflowHelpers.addNodeIds(newWorkflow);
|
||||
|
||||
let savedWorkflow: undefined | WorkflowEntity;
|
||||
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
|||
import { CommunityNodes1652254514003 } from './1652254514003-CommunityNodes';
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654090101303 } from './1654090101303-IntroducePinData';
|
||||
import { AddNodeIds1658932910559 } from './1658932910559-AddNodeIds';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -38,4 +39,5 @@ export const mysqlMigrations = [
|
|||
CommunityNodes1652254514003,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654090101303,
|
||||
AddNodeIds1658932910559,
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
|||
import { CommunityNodes1652254514002 } from './1652254514002-CommunityNodes';
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654090467022 } from './1654090467022-IntroducePinData';
|
||||
import { AddNodeIds1658932090381 } from './1658932090381-AddNodeIds';
|
||||
|
||||
export const postgresMigrations = [
|
||||
InitialMigration1587669153312,
|
||||
|
@ -34,4 +35,5 @@ export const postgresMigrations = [
|
|||
CommunityNodes1652254514002,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654090467022,
|
||||
AddNodeIds1658932090381,
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
|||
import { CommunityNodes1652254514001 } from './1652254514001-CommunityNodes'
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData';
|
||||
import { AddNodeIds1658930531669 } from './1658930531669-AddNodeIds';
|
||||
|
||||
const sqliteMigrations = [
|
||||
InitialMigration1588102412422,
|
||||
|
@ -32,6 +33,7 @@ const sqliteMigrations = [
|
|||
CommunityNodes1652254514001,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654089251344,
|
||||
AddNodeIds1658930531669,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
@ -951,6 +951,7 @@ test('POST /workflows should create workflow', async () => {
|
|||
name: 'testing',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -1047,6 +1048,7 @@ test('PUT /workflows/:id should fail due to non-existing workflow', async () =>
|
|||
name: 'testing',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
parameters: {},
|
||||
name: '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({
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -1120,6 +1123,7 @@ test('PUT /workflows/:id should update workflow', async () => {
|
|||
name: 'name updated',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -1127,6 +1131,7 @@ test('PUT /workflows/:id should update workflow', async () => {
|
|||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
parameters: {},
|
||||
name: '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',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: '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],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {},
|
||||
name: 'Cron',
|
||||
type: 'n8n-nodes-base.cron',
|
||||
|
|
|
@ -522,6 +522,7 @@ export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, u
|
|||
name: name ?? 'test workflow',
|
||||
nodes: nodes ?? [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Start',
|
||||
parameters: {},
|
||||
position: [-20, 260],
|
||||
|
@ -555,6 +556,7 @@ export async function createWorkflowWithTrigger(
|
|||
{
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -562,6 +564,7 @@ export async function createWorkflowWithTrigger(
|
|||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: { triggerTimes: { item: [{ mode: 'everyMinute' }] } },
|
||||
name: 'Cron',
|
||||
type: 'n8n-nodes-base.cron',
|
||||
|
@ -569,6 +572,7 @@ export async function createWorkflowWithTrigger(
|
|||
position: [500, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: { options: {} },
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
|
|
|
@ -91,6 +91,7 @@ function makeWorkflow({ withPinData }: { withPinData: boolean }) {
|
|||
workflow.connections = {};
|
||||
workflow.nodes = [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Spotify',
|
||||
type: 'n8n-nodes-base.spotify',
|
||||
parameters: { resource: 'track', operation: 'get', id: '123' },
|
||||
|
|
|
@ -194,6 +194,7 @@ describe('CredentialsHelper', () => {
|
|||
];
|
||||
|
||||
const node: INode = {
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'test',
|
||||
type: 'test.set',
|
||||
|
|
|
@ -58,6 +58,7 @@ export class LoadNodeParameterOptions {
|
|||
|
||||
const nodeData: INode = {
|
||||
parameters: currentNodeParameters,
|
||||
id: 'uuid-1234',
|
||||
name: TEMP_NODE_NAME,
|
||||
type: nodeTypeNameAndVersion.name,
|
||||
typeVersion: nodeTypeNameAndVersion.version,
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -44,6 +45,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [100, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -96,6 +98,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -103,6 +106,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [100, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -119,6 +123,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [300, 250],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -200,6 +205,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {
|
||||
mode: 'passThrough',
|
||||
},
|
||||
|
@ -209,6 +215,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [1150, 500],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -225,6 +232,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [290, 400],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -241,6 +249,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [850, 200],
|
||||
},
|
||||
{
|
||||
id: 'uuid-4',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -257,6 +266,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [650, 200],
|
||||
},
|
||||
{
|
||||
id: 'uuid-5',
|
||||
parameters: {
|
||||
mode: 'passThrough',
|
||||
},
|
||||
|
@ -266,6 +276,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [1150, 500],
|
||||
},
|
||||
{
|
||||
id: 'uuid-6',
|
||||
parameters: {},
|
||||
name: 'Merge3',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -273,6 +284,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [1000, 400],
|
||||
},
|
||||
{
|
||||
id: 'uuid-7',
|
||||
parameters: {
|
||||
mode: 'passThrough',
|
||||
output: 'input2',
|
||||
|
@ -283,6 +295,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [700, 400],
|
||||
},
|
||||
{
|
||||
id: 'uuid-8',
|
||||
parameters: {},
|
||||
name: 'Merge1',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -290,6 +303,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [500, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-9',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -306,6 +320,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [300, 200],
|
||||
},
|
||||
{
|
||||
id: 'uuid-10',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -526,6 +541,7 @@ describe('WorkflowExecute', () => {
|
|||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [250, 450],
|
||||
},
|
||||
{
|
||||
|
@ -543,6 +559,7 @@ describe('WorkflowExecute', () => {
|
|||
name: 'IF',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [650, 350],
|
||||
},
|
||||
{
|
||||
|
@ -550,6 +567,7 @@ describe('WorkflowExecute', () => {
|
|||
name: 'Merge1',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [1150, 450],
|
||||
},
|
||||
{
|
||||
|
@ -567,6 +585,7 @@ describe('WorkflowExecute', () => {
|
|||
name: 'Set1',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [450, 450],
|
||||
},
|
||||
{
|
||||
|
@ -584,6 +603,7 @@ describe('WorkflowExecute', () => {
|
|||
name: 'Set2',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [800, 250],
|
||||
},
|
||||
],
|
||||
|
@ -672,6 +692,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -679,6 +700,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [250, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {},
|
||||
name: 'Merge',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -686,6 +708,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [800, 450],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {},
|
||||
name: 'Merge1',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -693,6 +716,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [1000, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-4',
|
||||
parameters: {
|
||||
conditions: {
|
||||
boolean: [
|
||||
|
@ -716,6 +740,7 @@ describe('WorkflowExecute', () => {
|
|||
alwaysOutputData: false,
|
||||
},
|
||||
{
|
||||
id: 'uuid-5',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -738,6 +763,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [450, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-6',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -761,6 +787,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [450, 450],
|
||||
},
|
||||
{
|
||||
id: 'uuid-7',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -889,6 +916,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -896,6 +924,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [250, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {
|
||||
conditions: {
|
||||
number: [
|
||||
|
@ -913,6 +942,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [650, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {
|
||||
values: {
|
||||
string: [],
|
||||
|
@ -931,6 +961,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [850, 450],
|
||||
},
|
||||
{
|
||||
id: 'uuid-4',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -948,6 +979,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [450, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-5',
|
||||
parameters: {},
|
||||
name: 'Merge',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -1034,6 +1066,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -1041,6 +1074,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [250, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
|
@ -1057,6 +1091,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [450, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {},
|
||||
name: 'Merge',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
|
@ -1064,6 +1099,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [1050, 250],
|
||||
},
|
||||
{
|
||||
id: 'uuid-4',
|
||||
parameters: {
|
||||
conditions: {
|
||||
number: [
|
||||
|
@ -1081,6 +1117,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [650, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-5',
|
||||
parameters: {},
|
||||
name: 'NoOpTrue',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
|
@ -1088,6 +1125,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [850, 150],
|
||||
},
|
||||
{
|
||||
id: 'uuid-6',
|
||||
parameters: {},
|
||||
name: 'NoOpFalse',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
|
@ -1177,6 +1215,7 @@ describe('WorkflowExecute', () => {
|
|||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
|
@ -1184,6 +1223,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-2',
|
||||
parameters: {},
|
||||
name: 'VersionTest1a',
|
||||
type: 'n8n-nodes-base.versionTest',
|
||||
|
@ -1191,6 +1231,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [460, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-3',
|
||||
parameters: {
|
||||
versionTest: 11,
|
||||
},
|
||||
|
@ -1200,6 +1241,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [680, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-4',
|
||||
parameters: {},
|
||||
name: 'VersionTest2a',
|
||||
type: 'n8n-nodes-base.versionTest',
|
||||
|
@ -1207,6 +1249,7 @@ describe('WorkflowExecute', () => {
|
|||
position: [880, 300],
|
||||
},
|
||||
{
|
||||
id: 'uuid-5',
|
||||
parameters: {
|
||||
versionTest: 22,
|
||||
},
|
||||
|
|
|
@ -259,6 +259,12 @@ export interface IWorkflowDataUpdate {
|
|||
pinData?: IPinData;
|
||||
}
|
||||
|
||||
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
||||
meta?: {
|
||||
instanceId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkflowTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -866,7 +872,6 @@ export interface IRootState {
|
|||
workflowExecutionData: IExecutionResponse | null;
|
||||
lastSelectedNode: string | null;
|
||||
lastSelectedNodeOutputIndex: number | null;
|
||||
nodeIndex: Array<string | null>;
|
||||
nodeViewOffsetPosition: XYPosition;
|
||||
nodeViewMoveInProgress: boolean;
|
||||
selectedNodes: INodeUi[];
|
||||
|
|
|
@ -117,7 +117,7 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
|
||||
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) {
|
||||
this.closeDialog();
|
||||
|
|
|
@ -183,7 +183,7 @@ import {
|
|||
IExecutionResponse,
|
||||
IWorkflowDataUpdate,
|
||||
IMenuItem,
|
||||
IUser,
|
||||
IWorkflowToShare,
|
||||
} from '../Interface';
|
||||
|
||||
import ExecutionsList from '@/components/ExecutionsList.vue';
|
||||
|
@ -442,7 +442,6 @@ export default mixins(
|
|||
return;
|
||||
}
|
||||
|
||||
this.$telemetry.track('User imported workflow', { source: 'file', workflow_id: this.$store.getters.workflowId });
|
||||
this.$root.$emit('importWorkflowData', { data: worflowData });
|
||||
};
|
||||
|
||||
|
@ -513,8 +512,11 @@ export default mixins(
|
|||
data.id = parseInt(data.id, 10);
|
||||
}
|
||||
|
||||
const exportData: IWorkflowDataUpdate = {
|
||||
const exportData: IWorkflowToShare = {
|
||||
...data,
|
||||
meta: {
|
||||
instanceId: this.$store.getters.instanceId,
|
||||
},
|
||||
tags: (tags || []).map(tagId => {
|
||||
const {usageCount, ...tag} = this.$store.getters["tags/getTagById"](tagId);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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="{'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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="sticky-wrapper" :style="stickyPosition">
|
||||
<div class="sticky-wrapper" :style="stickyPosition" :id="nodeId">
|
||||
<div
|
||||
:class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}"
|
||||
:style="stickySize"
|
||||
|
@ -18,7 +18,7 @@
|
|||
:height="node.parameters.height"
|
||||
:width="node.parameters.width"
|
||||
:scale="nodeViewScale"
|
||||
:id="nodeIndex"
|
||||
:id="node.id"
|
||||
:readOnly="isReadOnly"
|
||||
:defaultText="defaultText"
|
||||
:editMode="isActive && !isReadOnly"
|
||||
|
@ -165,9 +165,9 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
if (!this.isSelected && this.node) {
|
||||
this.$emit('nodeSelected', this.node.name, false, true);
|
||||
}
|
||||
const nodeIndex = this.$store.getters.getNodeIndex(this.data.name);
|
||||
const nodeIdName = `node-${nodeIndex}`;
|
||||
this.instance.destroyDraggable(nodeIdName); // todo
|
||||
if (this.node) {
|
||||
this.instance.destroyDraggable(this.node.id); // todo avoid destroying if possible
|
||||
}
|
||||
},
|
||||
onResize({height, width, dX, dY}: { width: number, height: number, dX: number, dY: number }) {
|
||||
if (!this.node) {
|
||||
|
|
|
@ -3,12 +3,10 @@ import { INodeUi, XYPosition } from '@/Interface';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
import { getMousePosition, getRelativePosition } from '@/views/canvasHelpers';
|
||||
|
||||
export const mouseSelect = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
data () {
|
||||
return {
|
||||
|
@ -171,18 +169,15 @@ export const mouseSelect = mixins(
|
|||
|
||||
this.updateSelectBox(e);
|
||||
},
|
||||
|
||||
nodeDeselected (node: INodeUi) {
|
||||
this.$store.commit('removeNodeFromSelection', node);
|
||||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||
// @ts-ignore
|
||||
this.instance.removeFromDragSelection(nodeElement);
|
||||
this.instance.removeFromDragSelection(node.id);
|
||||
},
|
||||
nodeSelected (node: INodeUi) {
|
||||
this.$store.commit('addSelectedNode', node);
|
||||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||
// @ts-ignore
|
||||
this.instance.addToDragSelection(nodeElement);
|
||||
this.instance.addToDragSelection(node.id);
|
||||
},
|
||||
deselectAllNodes () {
|
||||
// @ts-ignore
|
||||
|
|
|
@ -2,12 +2,10 @@ import mixins from 'vue-typed-mixins';
|
|||
// @ts-ignore
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
import { getMousePosition } from '@/views/canvasHelpers';
|
||||
|
||||
export const moveNodeWorkflow = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -3,8 +3,7 @@ import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||
import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||
import * as CanvasHelpers from '@/views/canvasHelpers';
|
||||
import { Endpoint } from 'jsplumb';
|
||||
|
||||
|
@ -15,7 +14,6 @@ import { getStyleTokenValue } from '../helpers';
|
|||
|
||||
export const nodeBase = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
mounted () {
|
||||
// Initialize the node
|
||||
|
@ -28,10 +26,7 @@ export const nodeBase = mixins(
|
|||
return this.$store.getters.getNodeByName(this.name);
|
||||
},
|
||||
nodeId (): string {
|
||||
return NODE_NAME_PREFIX + this.nodeIndex;
|
||||
},
|
||||
nodeIndex (): string {
|
||||
return this.$store.getters.getNodeIndex(this.data.name).toString();
|
||||
return this.data.id;
|
||||
},
|
||||
},
|
||||
props: [
|
||||
|
@ -62,7 +57,7 @@ export const nodeBase = mixins(
|
|||
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
|
||||
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeIndex, index),
|
||||
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Rectangle',
|
||||
|
@ -71,7 +66,7 @@ export const nodeBase = mixins(
|
|||
isSource: false,
|
||||
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
|
||||
parameters: {
|
||||
nodeIndex: this.nodeIndex,
|
||||
nodeId: this.nodeId,
|
||||
type: inputName,
|
||||
index,
|
||||
},
|
||||
|
@ -130,7 +125,7 @@ export const nodeBase = mixins(
|
|||
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
|
||||
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
|
||||
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Dot',
|
||||
|
@ -140,7 +135,7 @@ export const nodeBase = mixins(
|
|||
isTarget: false,
|
||||
enabled: !this.isReadOnly,
|
||||
parameters: {
|
||||
nodeIndex: this.nodeIndex,
|
||||
nodeId: this.nodeId,
|
||||
type: inputName,
|
||||
index,
|
||||
},
|
||||
|
@ -166,7 +161,7 @@ export const nodeBase = mixins(
|
|||
|
||||
if (!this.isReadOnly) {
|
||||
const plusEndpointData: IEndpointOptions = {
|
||||
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
|
||||
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'N8nPlus',
|
||||
|
@ -187,7 +182,7 @@ export const nodeBase = mixins(
|
|||
hover: true, // hack to distinguish hover state
|
||||
},
|
||||
parameters: {
|
||||
nodeIndex: this.nodeIndex,
|
||||
nodeId: this.nodeId,
|
||||
type: inputName,
|
||||
index,
|
||||
},
|
||||
|
@ -258,8 +253,7 @@ export const nodeBase = mixins(
|
|||
// create a proper solution
|
||||
let newNodePositon: XYPosition;
|
||||
moveNodes.forEach((node: INodeUi) => {
|
||||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||
const element = document.getElementById(nodeElement);
|
||||
const element = document.getElementById(node.id);
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -53,7 +53,7 @@ import { showMessage } from '@/components/mixins/showMessage';
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const workflowHelpers = mixins(
|
||||
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 {
|
||||
this.$store.commit('addActiveAction', 'workflowSaving');
|
||||
|
||||
|
@ -674,10 +674,19 @@ export const workflowHelpers = mixins(
|
|||
// make sure that the new ones are not active
|
||||
workflowDataRequest.active = false;
|
||||
const changedNodes = {} as IDataObject;
|
||||
|
||||
if (resetNodeIds) {
|
||||
workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => {
|
||||
node.id = uuid();
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
if (resetWebhookUrls) {
|
||||
workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => {
|
||||
if (node.webhookId) {
|
||||
node.webhookId = uuidv4();
|
||||
node.webhookId = uuid();
|
||||
changedNodes[node.name] = node.webhookId;
|
||||
}
|
||||
return node;
|
||||
|
|
|
@ -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_DISPLAY_DATA_SIZE = 204800;
|
||||
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]';
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@ const state: IRootState = {
|
|||
workflowExecutionData: null,
|
||||
lastSelectedNode: null,
|
||||
lastSelectedNodeOutputIndex: null,
|
||||
nodeIndex: [],
|
||||
nodeViewOffsetPosition: [0, 0],
|
||||
nodeViewMoveInProgress: false,
|
||||
selectedNodes: [],
|
||||
|
@ -533,17 +532,6 @@ export const store = new Vuex.Store({
|
|||
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
|
||||
setNodeViewMoveInProgress(state, value: boolean) {
|
||||
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 => {
|
||||
return state.nodeViewOffsetPosition;
|
||||
},
|
||||
|
@ -838,7 +818,16 @@ export const store = new Vuex.Store({
|
|||
|
||||
// Selected Nodes
|
||||
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 => {
|
||||
let index;
|
||||
|
@ -874,6 +863,9 @@ export const store = new Vuex.Store({
|
|||
getNodeByName: (state, getters) => (nodeName: string): INodeUi | 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 => {
|
||||
for (const node of state.workflow.nodes) {
|
||||
if (node.issues === undefined || Object.keys(node.issues).length === 0) {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
class="node-view"
|
||||
:style="workflowStyle"
|
||||
>
|
||||
<div v-for="nodeData in nodes" :key="getNodeIndex(nodeData.name)">
|
||||
<div v-for="nodeData in nodes" :key="nodeData.id">
|
||||
<node
|
||||
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
||||
@duplicateNode="duplicateNode"
|
||||
|
@ -32,8 +32,7 @@
|
|||
@runWorkflow="onRunNode"
|
||||
@moved="onNodeMoved"
|
||||
@run="onNodeRun"
|
||||
:id="'node-' + getNodeIndex(nodeData.name)"
|
||||
:key="getNodeIndex(nodeData.name)"
|
||||
:key="nodeData.id"
|
||||
:name="nodeData.name"
|
||||
:isReadOnly="isReadOnly"
|
||||
:instance="instance"
|
||||
|
@ -46,7 +45,7 @@
|
|||
@deselectNode="nodeDeselectedByName"
|
||||
@nodeSelected="nodeSelectedByName"
|
||||
@removeNode="removeNode"
|
||||
:id="'node-' + getNodeIndex(nodeData.name)"
|
||||
:key="nodeData.id"
|
||||
:name="nodeData.name"
|
||||
:isReadOnly="isReadOnly"
|
||||
:instance="instance"
|
||||
|
@ -161,7 +160,6 @@ import {
|
|||
MODAL_CANCEL,
|
||||
MODAL_CLOSE,
|
||||
MODAL_CONFIRMED,
|
||||
NODE_NAME_PREFIX,
|
||||
NODE_OUTPUT_DEFAULT_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
ONBOARDING_PROMPT_TIMEBOX,
|
||||
|
@ -195,7 +193,7 @@ import Sticky from '@/components/Sticky.vue';
|
|||
import * as CanvasHelpers from './canvasHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuidv4} from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
IConnection,
|
||||
IConnections,
|
||||
|
@ -228,6 +226,7 @@ import {
|
|||
ITag,
|
||||
IWorkflowTemplate,
|
||||
IExecutionsSummary,
|
||||
IWorkflowToShare,
|
||||
} from '../Interface';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
|
@ -639,6 +638,7 @@ export default mixins(
|
|||
}
|
||||
this.resetWorkspace();
|
||||
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
|
||||
|
||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||
|
||||
if (data.workflow.pinData) {
|
||||
|
@ -1094,7 +1094,14 @@ export default mixins(
|
|||
|
||||
copySelectedNodes (isCut: boolean) {
|
||||
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);
|
||||
if (data.nodes.length > 0) {
|
||||
if(!isCut){
|
||||
|
@ -1290,11 +1297,7 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
this.$telemetry.track('User pasted nodes', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
});
|
||||
|
||||
return this.importWorkflowData(workflowData!, false);
|
||||
return this.importWorkflowData(workflowData!, false, 'paste');
|
||||
},
|
||||
|
||||
// Returns the workflow data from a given URL. If no data gets found or
|
||||
|
@ -1315,13 +1318,11 @@ export default mixins(
|
|||
}
|
||||
this.stopLoading();
|
||||
|
||||
this.$telemetry.track('User imported workflow', { source: 'url', workflow_id: this.$store.getters.workflowId });
|
||||
|
||||
return workflowData;
|
||||
},
|
||||
|
||||
// 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 (
|
||||
!workflowData.hasOwnProperty('nodes') ||
|
||||
|
@ -1331,6 +1332,40 @@ export default mixins(
|
|||
}
|
||||
|
||||
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
|
||||
// selected nodes and select the new ones
|
||||
this.deselectAllNodes();
|
||||
|
@ -1500,6 +1535,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
const newNodeData: INodeUi = {
|
||||
id: uuid(),
|
||||
name: nodeTypeData.defaults.name as string,
|
||||
type: nodeTypeData.name,
|
||||
typeVersion: Array.isArray(nodeTypeData.version)
|
||||
|
@ -1564,7 +1600,7 @@ export default mixins(
|
|||
});
|
||||
|
||||
if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) {
|
||||
newNodeData.webhookId = uuidv4();
|
||||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
await this.addNodes([newNodeData]);
|
||||
|
@ -1676,8 +1712,12 @@ export default mixins(
|
|||
// Get the node and set it as active that new nodes
|
||||
// which get created get automatically connected
|
||||
// to it.
|
||||
const sourceNodeName = this.$store.getters.getNodeNameByIndex(info.sourceId.slice(NODE_NAME_PREFIX.length));
|
||||
this.$store.commit('setLastSelectedNode', sourceNodeName);
|
||||
const sourceNode = this.$store.getters.getNodeById(info.sourceId) as INodeUi | null;
|
||||
if (!sourceNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('setLastSelectedNode', sourceNode.name);
|
||||
this.$store.commit('setLastSelectedNodeOutputIndex', info.index);
|
||||
this.newNodeInsertPosition = null;
|
||||
|
||||
|
@ -1696,7 +1736,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
|
||||
|
@ -1720,8 +1761,8 @@ export default mixins(
|
|||
// @ts-ignore
|
||||
const targetInfo = info.dropEndpoint.getParameters();
|
||||
|
||||
const sourceNodeName = this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex);
|
||||
const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex);
|
||||
const sourceNodeName = this.$store.getters.getNodeById(sourceInfo.nodeId).name;
|
||||
const targetNodeName = this.$store.getters.getNodeById(targetInfo.nodeId).name;
|
||||
|
||||
// check for duplicates
|
||||
if (this.getConnection(sourceNodeName, sourceInfo.index, targetNodeName, targetInfo.index)) {
|
||||
|
@ -1745,8 +1786,8 @@ export default mixins(
|
|||
const sourceInfo = info.sourceEndpoint.getParameters();
|
||||
const targetInfo = info.targetEndpoint.getParameters();
|
||||
|
||||
const sourceNodeName = this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex);
|
||||
const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex);
|
||||
const sourceNodeName = this.$store.getters.getNodeById(sourceInfo.nodeId).name;
|
||||
const targetNodeName = this.$store.getters.getNodeById(targetInfo.nodeId).name;
|
||||
|
||||
info.connection.__meta = {
|
||||
sourceNodeName,
|
||||
|
@ -1872,12 +1913,12 @@ export default mixins(
|
|||
|
||||
const connectionInfo = [
|
||||
{
|
||||
node: this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex),
|
||||
node: this.$store.getters.getNodeById(sourceInfo.nodeId).name,
|
||||
type: sourceInfo.type,
|
||||
index: sourceInfo.index,
|
||||
},
|
||||
{
|
||||
node: this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex),
|
||||
node: this.$store.getters.getNodeById(targetInfo.nodeId).name,
|
||||
type: targetInfo.type,
|
||||
index: targetInfo.index,
|
||||
},
|
||||
|
@ -1896,7 +1937,8 @@ export default mixins(
|
|||
this.__removeConnectionByConnectionInfo(info, false);
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) {
|
||||
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);
|
||||
|
||||
await this.addNodes([{...CanvasHelpers.DEFAULT_START_NODE}]);
|
||||
await this.addNodes([{
|
||||
id: uuid(),
|
||||
...CanvasHelpers.DEFAULT_START_NODE,
|
||||
}]);
|
||||
|
||||
this.nodeSelectedByName(CanvasHelpers.DEFAULT_START_NODE.name, false);
|
||||
|
||||
|
@ -2007,6 +2055,7 @@ export default mixins(
|
|||
this.$nextTick(async () => {
|
||||
await this.addNodes([
|
||||
{
|
||||
id: uuid(),
|
||||
...CanvasHelpers.WELCOME_STICKY_NODE,
|
||||
parameters: {
|
||||
// Use parameters from the template but add translated content
|
||||
|
@ -2108,17 +2157,33 @@ export default mixins(
|
|||
}
|
||||
});
|
||||
},
|
||||
getOutputEndpointUUID(nodeName: string, index: number) {
|
||||
return CanvasHelpers.getOutputEndpointUUID(this.getNodeIndex(nodeName), index);
|
||||
getOutputEndpointUUID(nodeName: string, index: number): string | null {
|
||||
const node = this.$store.getters.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CanvasHelpers.getOutputEndpointUUID(node.id, index);
|
||||
},
|
||||
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) {
|
||||
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] = [
|
||||
this.getOutputEndpointUUID(connection[0].node, connection[0].index),
|
||||
this.getInputEndpointUUID(connection[1].node, connection[1].index),
|
||||
outputUuid,
|
||||
inputUuid,
|
||||
];
|
||||
|
||||
// Create connections in DOM
|
||||
|
@ -2140,10 +2205,12 @@ export default mixins(
|
|||
},
|
||||
__removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) {
|
||||
if (removeVisualConnection === true) {
|
||||
const sourceId = this.$store.getters.getNodeByName(connection[0].node);
|
||||
const targetId = this.$store.getters.getNodeByName(connection[1].node);
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: NODE_NAME_PREFIX + this.getNodeIndex(connection[0].node),
|
||||
target: NODE_NAME_PREFIX + this.getNodeIndex(connection[1].node),
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -2175,12 +2242,12 @@ export default mixins(
|
|||
|
||||
const connectionInfo = [
|
||||
{
|
||||
node: this.$store.getters.getNodeNameByIndex(sourceInfo.nodeIndex),
|
||||
node: this.$store.getters.getNodeById(sourceInfo.nodeId).name,
|
||||
type: sourceInfo.type,
|
||||
index: sourceInfo.index,
|
||||
},
|
||||
{
|
||||
node: this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex),
|
||||
node: this.$store.getters.getNodeById(targetInfo.nodeId).name,
|
||||
type: targetInfo.type,
|
||||
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
|
||||
// not share objects
|
||||
const newNodeData = JSON.parse(JSON.stringify(this.getNodeDataToSave(node)));
|
||||
newNodeData.id = uuid();
|
||||
|
||||
// Check if node-name is unique else find one that is
|
||||
newNodeData.name = this.getUniqueNodeName({
|
||||
|
@ -2223,7 +2291,7 @@ export default mixins(
|
|||
|
||||
if (newNodeData.webhookId) {
|
||||
// Make sure that the node gets a new unique webhook-ID
|
||||
newNodeData.webhookId = uuidv4();
|
||||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
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 });
|
||||
},
|
||||
getJSPlumbConnection (sourceNodeName: string, sourceOutputIndex: number, targetNodeName: string, targetInputIndex: number): Connection | undefined {
|
||||
const sourceIndex = this.getNodeIndex(sourceNodeName);
|
||||
const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`;
|
||||
const sourceNode = this.$store.getters.getNodeByName(sourceNodeName) as INodeUi;
|
||||
const targetNode = this.$store.getters.getNodeByName(targetNodeName) as INodeUi;
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIndex = this.getNodeIndex(targetNodeName);
|
||||
const targetId = `${NODE_NAME_PREFIX}${targetIndex}`;
|
||||
const sourceId = sourceNode.id;
|
||||
const targetId = targetNode.id;
|
||||
|
||||
const sourceEndpoint = CanvasHelpers.getOutputEndpointUUID(sourceIndex, sourceOutputIndex);
|
||||
const targetEndpoint = CanvasHelpers.getInputEndpointUUID(targetIndex, targetInputIndex);
|
||||
const sourceEndpoint = CanvasHelpers.getOutputEndpointUUID(sourceId, sourceOutputIndex);
|
||||
const targetEndpoint = CanvasHelpers.getInputEndpointUUID(targetId, targetInputIndex);
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
|
@ -2269,9 +2340,8 @@ export default mixins(
|
|||
});
|
||||
},
|
||||
getJSPlumbEndpoints (nodeName: string): Endpoint[] {
|
||||
const nodeIndex = this.getNodeIndex(nodeName);
|
||||
const nodeId = `${NODE_NAME_PREFIX}${nodeIndex}`;
|
||||
return this.instance.getEndpoints(nodeId);
|
||||
const node = this.$store.getters.getNodeByName(nodeName);
|
||||
return this.instance.getEndpoints(node.id);
|
||||
},
|
||||
getPlusEndpoint (nodeName: string, outputIndex: number): Endpoint | undefined {
|
||||
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);
|
||||
},
|
||||
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
|
||||
const outgoing = this.instance.getConnections({
|
||||
source: name,
|
||||
source: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
// @ts-ignore
|
||||
const incoming = this.instance.getConnections({
|
||||
target: name,
|
||||
target: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
return {
|
||||
|
@ -2305,8 +2375,8 @@ export default mixins(
|
|||
},
|
||||
onNodeRun ({name, data, waiting}: {name: string, data: ITaskData[] | null, waiting: boolean}) {
|
||||
const sourceNodeName = name;
|
||||
const sourceIndex = this.$store.getters.getNodeIndex(sourceNodeName);
|
||||
const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`;
|
||||
const sourceNode = this.$store.getters.getNodeByName(sourceNodeName);
|
||||
const sourceId = sourceNode.id;
|
||||
|
||||
if (data === null || data.length === 0 || waiting) {
|
||||
// @ts-ignore
|
||||
|
@ -2438,18 +2508,15 @@ export default mixins(
|
|||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const nodeIndex = this.$store.getters.getNodeIndex(nodeName);
|
||||
const nodeIdName = `node-${nodeIndex}`;
|
||||
|
||||
// Suspend drawing
|
||||
this.instance.setSuspendDrawing(true);
|
||||
|
||||
// Remove all endpoints and the connections in jsplumb
|
||||
this.instance.removeAllEndpoints(nodeIdName);
|
||||
this.instance.removeAllEndpoints(node.id);
|
||||
|
||||
// Remove the draggable
|
||||
// @ts-ignore
|
||||
this.instance.destroyDraggable(nodeIdName);
|
||||
this.instance.destroyDraggable(node.id);
|
||||
|
||||
// Remove the connections in data
|
||||
this.$store.commit('removeAllNodeConnection', node);
|
||||
|
@ -2465,10 +2532,6 @@ export default mixins(
|
|||
// Remove node from selected index if found in it
|
||||
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
|
||||
},
|
||||
valueChanged (parameterData: IUpdateInformation) {
|
||||
|
@ -2557,7 +2620,7 @@ export default mixins(
|
|||
try {
|
||||
const nodes = this.$store.getters.allNodes as INodeUi[];
|
||||
// @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();
|
||||
} catch (e) {}
|
||||
|
@ -2621,6 +2684,10 @@ export default mixins(
|
|||
let nodeType: INodeTypeDescription | null;
|
||||
let foundNodeIssues: INodeIssues | null;
|
||||
nodes.forEach((node) => {
|
||||
if (!node.id) {
|
||||
node.id = uuid();
|
||||
}
|
||||
|
||||
nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null;
|
||||
|
||||
// Make sure that some properties always exist
|
||||
|
@ -2919,7 +2986,6 @@ export default mixins(
|
|||
this.$store.commit('removeActiveAction', 'workflowRunning');
|
||||
this.$store.commit('setExecutionWaitingForWebhook', false);
|
||||
|
||||
this.$store.commit('resetNodeIndex');
|
||||
this.$store.commit('resetSelectedNodes');
|
||||
|
||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false});
|
||||
|
@ -2983,19 +3049,24 @@ export default mixins(
|
|||
}
|
||||
},
|
||||
async onImportWorkflowDataEvent(data: IDataObject) {
|
||||
await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
||||
await this.importWorkflowData(data.data as IWorkflowDataUpdate, undefined, 'file');
|
||||
},
|
||||
async onImportWorkflowUrlEvent(data: IDataObject) {
|
||||
const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
|
||||
if (workflowData !== undefined) {
|
||||
await this.importWorkflowData(workflowData);
|
||||
await this.importWorkflowData(workflowData, undefined, 'url');
|
||||
}
|
||||
},
|
||||
addPinDataConnections(pinData: IPinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = this.$store.getters.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName),
|
||||
source: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
connections.forEach((connection) => {
|
||||
|
@ -3008,9 +3079,14 @@ export default mixins(
|
|||
},
|
||||
removePinDataConnections(pinData: IPinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = this.$store.getters.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName),
|
||||
source: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
connections.forEach(CanvasHelpers.resetConnection);
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
NodeInputConnections,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const OVERLAY_DROP_NODE_ID = 'drop-add-node';
|
||||
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) => {
|
||||
return `${nodeIndex}${OUTPUT_UUID_KEY}${outputIndex}`;
|
||||
export const getOutputEndpointUUID = (nodeId: string, outputIndex: number) => {
|
||||
return `${nodeId}${OUTPUT_UUID_KEY}${outputIndex}`;
|
||||
};
|
||||
|
||||
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
||||
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
||||
export const getInputEndpointUUID = (nodeId: string, inputIndex: number) => {
|
||||
return `${nodeId}${INPUT_UUID_KEY}${inputIndex}`;
|
||||
};
|
||||
|
||||
export const getFixedNodesList = (workflowNodes: INode[]) => {
|
||||
|
@ -728,7 +729,7 @@ export const getFixedNodesList = (workflowNodes: INode[]) => {
|
|||
});
|
||||
|
||||
if (!hasStartNode) {
|
||||
nodes.push({...DEFAULT_START_NODE});
|
||||
nodes.push({...DEFAULT_START_NODE, id: uuid() });
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
|
|
@ -823,6 +823,7 @@ export interface INodeCredentials {
|
|||
}
|
||||
|
||||
export interface INode {
|
||||
id: string;
|
||||
name: string;
|
||||
typeVersion: number;
|
||||
type: string;
|
||||
|
@ -1542,6 +1543,7 @@ export interface INoteGraphItem {
|
|||
}
|
||||
|
||||
export interface INodeGraphItem {
|
||||
id: string;
|
||||
type: string;
|
||||
resource?: string;
|
||||
operation?: string;
|
||||
|
@ -1553,6 +1555,8 @@ export interface INodeGraphItem {
|
|||
credential_type?: string; // HTTP Request node v2
|
||||
credential_set?: boolean; // HTTP Request node v2
|
||||
method?: string; // HTTP Request node v2
|
||||
src_node_id?: string;
|
||||
src_instance_id?: string;
|
||||
}
|
||||
|
||||
export interface INodeNameIndex {
|
||||
|
|
|
@ -22,10 +22,10 @@ export function isNumber(value: unknown): value is number {
|
|||
}
|
||||
|
||||
function getStickyDimensions(note: INode, stickyType: INodeType | undefined) {
|
||||
const heightProperty = stickyType?.description.properties.find(
|
||||
const heightProperty = stickyType?.description?.properties.find(
|
||||
(property) => property.name === 'height',
|
||||
);
|
||||
const widthProperty = stickyType?.description.properties.find(
|
||||
const widthProperty = stickyType?.description?.properties.find(
|
||||
(property) => property.name === 'width',
|
||||
);
|
||||
|
||||
|
@ -114,6 +114,10 @@ export function getDomainPath(raw: string, urlParts = URL_PARTS_REGEX): string {
|
|||
export function generateNodesGraph(
|
||||
workflow: IWorkflowBase,
|
||||
nodeTypes: INodeTypes,
|
||||
options?: {
|
||||
sourceInstanceId?: string;
|
||||
nodeIdMap?: { [curr: string]: string };
|
||||
},
|
||||
): INodesGraphResult {
|
||||
const nodesGraph: INodesGraph = {
|
||||
node_types: [],
|
||||
|
@ -149,10 +153,19 @@ export function generateNodesGraph(
|
|||
otherNodes.forEach((node: INode, index: number) => {
|
||||
nodesGraph.node_types.push(node.type);
|
||||
const nodeItem: INodeGraphItem = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
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) {
|
||||
try {
|
||||
nodeItem.domain = new URL(node.parameters.url as string).hostname;
|
||||
|
@ -182,7 +195,7 @@ export function generateNodesGraph(
|
|||
} else {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(node.type);
|
||||
|
||||
nodeType?.description.properties.forEach((property) => {
|
||||
nodeType?.description?.properties?.forEach((property) => {
|
||||
if (
|
||||
property.name === 'operation' ||
|
||||
property.name === 'resource' ||
|
||||
|
@ -212,7 +225,7 @@ export function generateNodesGraph(
|
|||
});
|
||||
});
|
||||
});
|
||||
} catch (_) {
|
||||
} catch (e) {
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ describe('Expression', () => {
|
|||
name: 'node',
|
||||
typeVersion: 1,
|
||||
type: 'test.set',
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
}
|
||||
|
|
|
@ -613,6 +613,7 @@ describe('RoutingNode', () => {
|
|||
name: 'test',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
|
@ -1659,6 +1660,7 @@ describe('RoutingNode', () => {
|
|||
name: 'test',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
|
@ -1831,6 +1833,7 @@ describe('RoutingNode', () => {
|
|||
name: 'test',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
|
|
|
@ -548,6 +548,7 @@ describe('Workflow', () => {
|
|||
parameters: stubData.parameters,
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1234',
|
||||
position: [100, 100],
|
||||
};
|
||||
}
|
||||
|
@ -1008,6 +1009,7 @@ describe('Workflow', () => {
|
|||
parameters: testData.input.Node1.parameters,
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [100, 100],
|
||||
},
|
||||
{
|
||||
|
@ -1015,6 +1017,7 @@ describe('Workflow', () => {
|
|||
parameters: testData.input.Node2.parameters,
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
|
@ -1026,6 +1029,7 @@ describe('Workflow', () => {
|
|||
: {},
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [100, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1037,6 +1041,7 @@ describe('Workflow', () => {
|
|||
: {},
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [100, 400],
|
||||
},
|
||||
];
|
||||
|
@ -1219,6 +1224,7 @@ describe('Workflow', () => {
|
|||
},
|
||||
type: 'test.setMulti',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1234',
|
||||
position: [100, 100],
|
||||
},
|
||||
];
|
||||
|
@ -1296,6 +1302,7 @@ describe('Workflow', () => {
|
|||
name: 'Start',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1305,6 +1312,7 @@ describe('Workflow', () => {
|
|||
name: 'Set',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [460, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1314,6 +1322,7 @@ describe('Workflow', () => {
|
|||
name: 'Set1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [680, 300],
|
||||
},
|
||||
],
|
||||
|
@ -1353,6 +1362,7 @@ describe('Workflow', () => {
|
|||
name: 'Switch',
|
||||
type: 'test.switch',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [460, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1362,6 +1372,7 @@ describe('Workflow', () => {
|
|||
name: 'Set',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [740, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1371,6 +1382,7 @@ describe('Workflow', () => {
|
|||
name: 'Set1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [780, 100],
|
||||
},
|
||||
{
|
||||
|
@ -1380,6 +1392,7 @@ describe('Workflow', () => {
|
|||
name: 'Set2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [1040, 260],
|
||||
},
|
||||
],
|
||||
|
@ -1443,6 +1456,7 @@ describe('Workflow', () => {
|
|||
name: 'Switch',
|
||||
type: 'test.switch',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [920, 340],
|
||||
},
|
||||
{
|
||||
|
@ -1450,6 +1464,7 @@ describe('Workflow', () => {
|
|||
name: 'Start',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [240, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1459,6 +1474,7 @@ describe('Workflow', () => {
|
|||
name: 'Set1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [700, 340],
|
||||
},
|
||||
{
|
||||
|
@ -1468,6 +1484,7 @@ describe('Workflow', () => {
|
|||
name: 'Set',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [1220, 300],
|
||||
},
|
||||
{
|
||||
|
@ -1475,6 +1492,7 @@ describe('Workflow', () => {
|
|||
name: 'Switch',
|
||||
type: 'test.switch',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-5',
|
||||
position: [920, 340],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -10,6 +10,7 @@ describe('WorkflowDataProxy', () => {
|
|||
name: 'Start',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
|
@ -20,6 +21,7 @@ describe('WorkflowDataProxy', () => {
|
|||
name: 'Function',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [280, 200],
|
||||
},
|
||||
{
|
||||
|
@ -36,6 +38,7 @@ describe('WorkflowDataProxy', () => {
|
|||
name: 'Rename',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [460, 200],
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue