mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 14:44:05 -08:00
🔀 Merge branch 'master' into fix/http-request-node
This commit is contained in:
commit
d0bde5e0c6
|
@ -6,6 +6,8 @@ if [ -d /root/.n8n ] ; then
|
|||
ln -s /root/.n8n /home/node/
|
||||
fi
|
||||
|
||||
chown -R node /home/node
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
# Got started with arguments
|
||||
COMMAND=$1;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
InternalHooksManager,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
|
@ -123,6 +124,9 @@ export class Execute extends Command {
|
|||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
InternalHooksManager.init(instanceId);
|
||||
|
||||
// Add the found types to an instance other parts of the application can use
|
||||
const nodeTypes = NodeTypes();
|
||||
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
InternalHooksManager,
|
||||
IWorkflowDb,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
|
@ -55,12 +56,12 @@ export class ExecuteBatch extends Command {
|
|||
static executionTimeout = 3 * 60 * 1000;
|
||||
|
||||
static examples = [
|
||||
`$ n8n executeAll`,
|
||||
`$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`,
|
||||
`$ n8n executeAll --debug --output=/data/output.json`,
|
||||
`$ n8n executeAll --ids=10,13,15 --shortOutput`,
|
||||
`$ n8n executeAll --snapshot=/data/snapshots --shallow`,
|
||||
`$ n8n executeAll --compare=/data/previousExecutionData --retries=2`,
|
||||
`$ n8n executeBatch`,
|
||||
`$ n8n executeBatch --concurrency=10 --skipList=/data/skipList.txt`,
|
||||
`$ n8n executeBatch --debug --output=/data/output.json`,
|
||||
`$ n8n executeBatch --ids=10,13,15 --shortOutput`,
|
||||
`$ n8n executeBatch --snapshot=/data/snapshots --shallow`,
|
||||
`$ n8n executeBatch --compare=/data/previousExecutionData --retries=2`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
|
@ -303,6 +304,9 @@ export class ExecuteBatch extends Command {
|
|||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
InternalHooksManager.init(instanceId);
|
||||
|
||||
// Add the found types to an instance other parts of the application can use
|
||||
const nodeTypes = NodeTypes();
|
||||
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
|
||||
|
@ -813,10 +817,22 @@ export class ExecuteBatch extends Command {
|
|||
const changes = diff(JSON.parse(contents), data, { keysOnly: true });
|
||||
|
||||
if (changes !== undefined) {
|
||||
// we have structural changes. Report them.
|
||||
executionResult.error = `Workflow may contain breaking changes`;
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'error';
|
||||
// If we had only additions with no removals
|
||||
// Then we treat as a warning and not an error.
|
||||
// To find this, we convert the object to JSON
|
||||
// and search for the `__deleted` string
|
||||
const changesJson = JSON.stringify(changes);
|
||||
if (changesJson.includes('__deleted')) {
|
||||
// we have structural changes. Report them.
|
||||
executionResult.error = 'Workflow may contain breaking changes';
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'error';
|
||||
} else {
|
||||
executionResult.error =
|
||||
'Workflow contains new data that previously did not exist.';
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'warning';
|
||||
}
|
||||
} else {
|
||||
executionResult.executionStatus = 'success';
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import * as PCancelable from 'p-cancelable';
|
|||
import { Command, flags } from '@oclif/command';
|
||||
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import { INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
|
||||
import { IExecuteResponsePromiseData, INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { FindOneOptions } from 'typeorm';
|
||||
|
||||
|
@ -25,11 +25,13 @@ import {
|
|||
GenericHelpers,
|
||||
IBullJobData,
|
||||
IBullJobResponse,
|
||||
IBullWebhookResponse,
|
||||
IExecutionFlattedDb,
|
||||
InternalHooksManager,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
} from '../src';
|
||||
|
||||
|
@ -172,6 +174,16 @@ export class Worker extends Command {
|
|||
currentExecutionDb.workflowData,
|
||||
{ retryOf: currentExecutionDb.retryOf as string },
|
||||
);
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
await job.progress({
|
||||
executionId: job.data.executionId as string,
|
||||
response: WebhookHelpers.encodeWebhookResponse(response),
|
||||
} as IBullWebhookResponse);
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.executionId = jobData.executionId;
|
||||
|
||||
let workflowExecute: WorkflowExecute;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.145.0",
|
||||
"version": "0.149.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -83,7 +83,7 @@
|
|||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@rudderstack/rudder-sdk-node": "^1.0.2",
|
||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^8.5.2",
|
||||
"basic-auth": "^2.0.1",
|
||||
|
@ -110,10 +110,10 @@
|
|||
"localtunnel": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mysql2": "~2.3.0",
|
||||
"n8n-core": "~0.90.0",
|
||||
"n8n-editor-ui": "~0.113.0",
|
||||
"n8n-nodes-base": "~0.142.0",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-core": "~0.93.0",
|
||||
"n8n-editor-ui": "~0.116.0",
|
||||
"n8n-nodes-base": "~0.146.0",
|
||||
"n8n-workflow": "~0.76.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
"pg": "^8.3.0",
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { IRun } from 'n8n-workflow';
|
||||
|
||||
import { createDeferredPromise } from 'n8n-core';
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ChildProcess } from 'child_process';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
|
@ -116,6 +119,28 @@ export class ActiveExecutions {
|
|||
this.activeExecutions[executionId].workflowExecution = workflowExecution;
|
||||
}
|
||||
|
||||
attachResponsePromise(
|
||||
executionId: string,
|
||||
responsePromise: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): void {
|
||||
if (this.activeExecutions[executionId] === undefined) {
|
||||
throw new Error(
|
||||
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
|
||||
);
|
||||
}
|
||||
|
||||
this.activeExecutions[executionId].responsePromise = responsePromise;
|
||||
}
|
||||
|
||||
resolveResponsePromise(executionId: string, response: IExecuteResponsePromiseData): void {
|
||||
if (this.activeExecutions[executionId] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
this.activeExecutions[executionId].responsePromise?.resolve(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an active execution
|
||||
*
|
||||
|
@ -193,6 +218,7 @@ export class ActiveExecutions {
|
|||
|
||||
this.activeExecutions[executionId].postExecutePromises.push(waitPromise);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return waitPromise.promise();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
IGetExecutePollFunctions,
|
||||
IGetExecuteTriggerFunctions,
|
||||
INode,
|
||||
|
@ -40,8 +42,6 @@ import {
|
|||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
|
@ -550,6 +550,7 @@ export class ActiveWorkflowRunner {
|
|||
data: INodeExecutionData[][],
|
||||
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||
mode: WorkflowExecuteMode,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
) {
|
||||
const nodeExecutionStack: IExecuteData[] = [
|
||||
{
|
||||
|
@ -580,7 +581,7 @@ export class ActiveWorkflowRunner {
|
|||
};
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
return workflowRunner.run(runData, true);
|
||||
return workflowRunner.run(runData, true, undefined, undefined, responsePromise);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -641,13 +642,16 @@ export class ActiveWorkflowRunner {
|
|||
mode,
|
||||
activation,
|
||||
);
|
||||
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
||||
returnFunctions.emit = (
|
||||
data: INodeExecutionData[][],
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
||||
WorkflowHelpers.saveStaticData(workflow);
|
||||
// eslint-disable-next-line id-denylist
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) =>
|
||||
console.error(err),
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode, responsePromise).catch(
|
||||
(error) => console.error(error),
|
||||
);
|
||||
};
|
||||
return returnFunctions;
|
||||
|
|
|
@ -7,19 +7,19 @@ import {
|
|||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ITelemetrySettings,
|
||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
IWorkflowCredentials,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
|
||||
import { WorkflowExecute } from 'n8n-core';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as PCancelable from 'p-cancelable';
|
||||
|
@ -47,6 +47,11 @@ export interface IBullJobResponse {
|
|||
success: boolean;
|
||||
}
|
||||
|
||||
export interface IBullWebhookResponse {
|
||||
executionId: string;
|
||||
response: IExecuteResponsePromiseData;
|
||||
}
|
||||
|
||||
export interface ICustomRequest extends Request {
|
||||
parsedUrl: Url | undefined;
|
||||
}
|
||||
|
@ -237,6 +242,7 @@ export interface IExecutingWorkflowData {
|
|||
process?: ChildProcess;
|
||||
startedAt: Date;
|
||||
postExecutePromises: Array<IDeferredPromise<IRun | undefined>>;
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>;
|
||||
workflowExecution?: PCancelable<IRun>;
|
||||
}
|
||||
|
||||
|
@ -490,6 +496,7 @@ export interface IPushDataConsoleMessage {
|
|||
|
||||
export interface IResponseCallbackData {
|
||||
data?: IDataObject | IDataObject[];
|
||||
headers?: object;
|
||||
noWebhookResponse?: boolean;
|
||||
responseCode?: number;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ class NodeTypesClass implements INodeTypes {
|
|||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
if (this.nodeTypes[nodeType] === undefined) {
|
||||
throw new Error(`The node-type "${nodeType}" is not known!`);
|
||||
}
|
||||
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import * as Bull from 'bull';
|
||||
import * as config from '../config';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { IBullJobData } from './Interfaces';
|
||||
import { IBullJobData, IBullWebhookResponse } from './Interfaces';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as ActiveExecutions from './ActiveExecutions';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as WebhookHelpers from './WebhookHelpers';
|
||||
|
||||
export class Queue {
|
||||
private activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||
|
||||
private jobQueue: Bull.Queue;
|
||||
|
||||
constructor() {
|
||||
this.activeExecutions = ActiveExecutions.getInstance();
|
||||
|
||||
const prefix = config.get('queue.bull.prefix') as string;
|
||||
const redisOptions = config.get('queue.bull.redis') as object;
|
||||
// Disabling ready check is necessary as it allows worker to
|
||||
|
@ -16,6 +25,14 @@ export class Queue {
|
|||
// More here: https://github.com/OptimalBits/bull/issues/890
|
||||
// @ts-ignore
|
||||
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
|
||||
|
||||
this.jobQueue.on('global:progress', (jobId, progress: IBullWebhookResponse) => {
|
||||
this.activeExecutions.resolveResponsePromise(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
progress.executionId,
|
||||
WebhookHelpers.decodeWebhookResponse(progress.response),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
|
||||
|
|
|
@ -72,11 +72,16 @@ export function sendSuccessResponse(
|
|||
data: any,
|
||||
raw?: boolean,
|
||||
responseCode?: number,
|
||||
responseHeader?: object,
|
||||
) {
|
||||
if (responseCode !== undefined) {
|
||||
res.status(responseCode);
|
||||
}
|
||||
|
||||
if (responseHeader) {
|
||||
res.header(responseHeader);
|
||||
}
|
||||
|
||||
if (raw === true) {
|
||||
if (typeof data === 'string') {
|
||||
res.send(data);
|
||||
|
|
|
@ -679,6 +679,7 @@ class App {
|
|||
|
||||
// @ts-ignore
|
||||
savedWorkflow.id = savedWorkflow.id.toString();
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow as IWorkflowBase);
|
||||
return savedWorkflow;
|
||||
},
|
||||
|
@ -1579,11 +1580,11 @@ class App {
|
|||
const findQuery = {} as FindManyOptions;
|
||||
if (req.query.filter) {
|
||||
findQuery.where = JSON.parse(req.query.filter as string);
|
||||
if ((findQuery.where! as IDataObject).id !== undefined) {
|
||||
if (findQuery.where.id !== undefined) {
|
||||
// No idea if multiple where parameters make db search
|
||||
// slower but to be sure that that is not the case we
|
||||
// remove all unnecessary fields in case the id is defined.
|
||||
findQuery.where = { id: (findQuery.where! as IDataObject).id };
|
||||
findQuery.where = { id: findQuery.where.id };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2668,7 +2669,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2719,7 +2726,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2745,7 +2758,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
|
@ -18,9 +19,13 @@ import { get } from 'lodash';
|
|||
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
IN8nHttpFullResponse,
|
||||
INode,
|
||||
IRunExecutionData,
|
||||
IWebhookData,
|
||||
|
@ -34,20 +39,20 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
ActiveExecutions,
|
||||
GenericHelpers,
|
||||
IExecutionDb,
|
||||
IResponseCallbackData,
|
||||
IWorkflowDb,
|
||||
IWorkflowExecutionDataProcess,
|
||||
ResponseHelper,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
} from '.';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as ActiveExecutions from './ActiveExecutions';
|
||||
|
||||
const activeExecutions = ActiveExecutions.getInstance();
|
||||
|
||||
/**
|
||||
|
@ -91,6 +96,35 @@ export function getWorkflowWebhooks(
|
|||
return returnData;
|
||||
}
|
||||
|
||||
export function decodeWebhookResponse(
|
||||
response: IExecuteResponsePromiseData,
|
||||
): IExecuteResponsePromiseData {
|
||||
if (
|
||||
typeof response === 'object' &&
|
||||
typeof response.body === 'object' &&
|
||||
(response.body as IDataObject)['__@N8nEncodedBuffer@__']
|
||||
) {
|
||||
response.body = Buffer.from(
|
||||
(response.body as IDataObject)['__@N8nEncodedBuffer@__'] as string,
|
||||
BINARY_ENCODING,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export function encodeWebhookResponse(
|
||||
response: IExecuteResponsePromiseData,
|
||||
): IExecuteResponsePromiseData {
|
||||
if (typeof response === 'object' && Buffer.isBuffer(response.body)) {
|
||||
response.body = {
|
||||
'__@N8nEncodedBuffer@__': response.body.toString(BINARY_ENCODING),
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the webhooks which should be created for the give workflow
|
||||
*
|
||||
|
@ -169,7 +203,7 @@ export async function executeWebhook(
|
|||
200,
|
||||
) as number;
|
||||
|
||||
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
|
||||
if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode as string)) {
|
||||
// If the mode is not known we error. Is probably best like that instead of using
|
||||
// the default that people know as early as possible (probably already testing phase)
|
||||
// that something does not resolve properly.
|
||||
|
@ -356,9 +390,52 @@ export async function executeWebhook(
|
|||
workflowData,
|
||||
};
|
||||
|
||||
let responsePromise: IDeferredPromise<IN8nHttpFullResponse> | undefined;
|
||||
if (responseMode === 'responseNode') {
|
||||
responsePromise = await createDeferredPromise<IN8nHttpFullResponse>();
|
||||
responsePromise
|
||||
.promise()
|
||||
.then((response: IN8nHttpFullResponse) => {
|
||||
if (didSendResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(response.body)) {
|
||||
res.header(response.headers);
|
||||
res.end(response.body);
|
||||
|
||||
responseCallback(null, {
|
||||
noWebhookResponse: true,
|
||||
});
|
||||
} else {
|
||||
// TODO: This probably needs some more changes depending on the options on the
|
||||
// Webhook Response node
|
||||
responseCallback(null, {
|
||||
data: response.body as IDataObject,
|
||||
headers: response.headers,
|
||||
responseCode: response.statusCode,
|
||||
});
|
||||
}
|
||||
|
||||
didSendResponse = true;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
Logger.error(
|
||||
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
|
||||
{ executionId, workflowId: workflow.id },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Start now to run the workflow
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
|
||||
executionId = await workflowRunner.run(
|
||||
runData,
|
||||
true,
|
||||
!didSendResponse,
|
||||
executionId,
|
||||
responsePromise,
|
||||
);
|
||||
|
||||
Logger.verbose(
|
||||
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
||||
|
@ -398,6 +475,20 @@ export async function executeWebhook(
|
|||
return data;
|
||||
}
|
||||
|
||||
if (responseMode === 'responseNode') {
|
||||
if (!didSendResponse) {
|
||||
// Return an error if no Webhook-Response node did send any data
|
||||
responseCallback(null, {
|
||||
data: {
|
||||
message: 'Workflow executed sucessfully.',
|
||||
},
|
||||
responseCode,
|
||||
});
|
||||
didSendResponse = true;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (returnData === undefined) {
|
||||
if (!didSendResponse) {
|
||||
responseCallback(null, {
|
||||
|
|
|
@ -64,7 +64,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -115,7 +121,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -141,7 +153,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -173,7 +191,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -199,7 +223,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -225,7 +255,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
|||
|
||||
import {
|
||||
ExecutionError,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
LoggerProxy as Logger,
|
||||
Workflow,
|
||||
|
@ -41,9 +43,7 @@ import {
|
|||
IBullJobResponse,
|
||||
ICredentialsOverwrite,
|
||||
ICredentialsTypeData,
|
||||
IExecutionDb,
|
||||
IExecutionFlattedDb,
|
||||
IExecutionResponse,
|
||||
IProcessMessageDataHook,
|
||||
ITransferNodeTypes,
|
||||
IWorkflowExecutionDataProcess,
|
||||
|
@ -51,6 +51,7 @@ import {
|
|||
NodeTypes,
|
||||
Push,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
|
@ -146,6 +147,7 @@ export class WorkflowRunner {
|
|||
loadStaticData?: boolean,
|
||||
realtime?: boolean,
|
||||
executionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
const executionsProcess = config.get('executions.process') as string;
|
||||
const executionsMode = config.get('executions.mode') as string;
|
||||
|
@ -153,11 +155,17 @@ export class WorkflowRunner {
|
|||
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
|
||||
// Do not run "manual" executions in bull because sending events to the
|
||||
// frontend would not be possible
|
||||
executionId = await this.runBull(data, loadStaticData, realtime, executionId);
|
||||
executionId = await this.runBull(
|
||||
data,
|
||||
loadStaticData,
|
||||
realtime,
|
||||
executionId,
|
||||
responsePromise,
|
||||
);
|
||||
} else if (executionsProcess === 'main') {
|
||||
executionId = await this.runMainProcess(data, loadStaticData, executionId);
|
||||
executionId = await this.runMainProcess(data, loadStaticData, executionId, responsePromise);
|
||||
} else {
|
||||
executionId = await this.runSubprocess(data, loadStaticData, executionId);
|
||||
executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise);
|
||||
}
|
||||
|
||||
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
|
||||
|
@ -200,6 +208,7 @@ export class WorkflowRunner {
|
|||
data: IWorkflowExecutionDataProcess,
|
||||
loadStaticData?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
if (loadStaticData === true && data.workflowData.id) {
|
||||
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
|
||||
|
@ -256,6 +265,15 @@ export class WorkflowRunner {
|
|||
executionId,
|
||||
true,
|
||||
);
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
if (responsePromise) {
|
||||
responsePromise.resolve(response);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
@ -341,11 +359,15 @@ export class WorkflowRunner {
|
|||
loadStaticData?: boolean,
|
||||
realtime?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
// TODO: If "loadStaticData" is set to true it has to load data new on worker
|
||||
|
||||
// Register the active execution
|
||||
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
|
||||
if (responsePromise) {
|
||||
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
|
||||
}
|
||||
|
||||
const jobData: IBullJobData = {
|
||||
executionId,
|
||||
|
@ -545,6 +567,7 @@ export class WorkflowRunner {
|
|||
data: IWorkflowExecutionDataProcess,
|
||||
loadStaticData?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
let startedAt = new Date();
|
||||
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
||||
|
@ -653,6 +676,10 @@ export class WorkflowRunner {
|
|||
} else if (message.type === 'end') {
|
||||
clearTimeout(executionTimeout);
|
||||
this.activeExecutions.remove(executionId, message.data.runData);
|
||||
} else if (message.type === 'sendResponse') {
|
||||
if (responsePromise) {
|
||||
responsePromise.resolve(WebhookHelpers.decodeWebhookResponse(message.data.response));
|
||||
}
|
||||
} else if (message.type === 'sendMessageToUI') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(
|
||||
|
|
|
@ -10,6 +10,7 @@ import { IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
|
|||
import {
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteWorkflowInfo,
|
||||
ILogger,
|
||||
INodeExecutionData,
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
IWorkflowExecuteProcess,
|
||||
IWorkflowExecutionDataProcessWithExecution,
|
||||
NodeTypes,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
|
@ -200,6 +202,15 @@ export class WorkflowRunnerProcess {
|
|||
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
|
||||
);
|
||||
additionalData.hooks = this.getProcessForwardHooks();
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
await sendToParentProcess('sendResponse', {
|
||||
response: WebhookHelpers.encodeWebhookResponse(response),
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.executionId = inputData.executionId;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
39
packages/cli/src/databases/MigrationHelpers.ts
Normal file
39
packages/cli/src/databases/MigrationHelpers.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
export class MigrationHelpers {
|
||||
queryRunner: QueryRunner;
|
||||
|
||||
constructor(queryRunner: QueryRunner) {
|
||||
this.queryRunner = queryRunner;
|
||||
}
|
||||
|
||||
// runs an operation sequential on chunks of a query that returns a potentially large Array.
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async runChunked(
|
||||
query: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
operation: (results: any[]) => Promise<void>,
|
||||
limit = 100,
|
||||
): Promise<void> {
|
||||
let offset = 0;
|
||||
let chunkedQuery: string;
|
||||
let chunkedQueryResults: unknown[];
|
||||
|
||||
do {
|
||||
chunkedQuery = this.chunkQuery(query, limit, offset);
|
||||
chunkedQueryResults = (await this.queryRunner.query(chunkedQuery)) as unknown[];
|
||||
// pass a copy to prevent errors from mutation
|
||||
await operation([...chunkedQueryResults]);
|
||||
offset += limit;
|
||||
} while (chunkedQueryResults.length === limit);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
private chunkQuery(query: string, limit: number, offset = 0): string {
|
||||
return `
|
||||
${query}
|
||||
LIMIT ${limit}
|
||||
OFFSET ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,58 +9,100 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630451444017';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
|
@ -68,8 +111,8 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -78,7 +121,6 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
|
@ -92,77 +134,124 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
|
@ -171,8 +260,8 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -200,15 +289,15 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,62 +9,104 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630419189837';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -73,7 +116,8 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -104,9 +148,10 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
|
@ -115,62 +160,109 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -179,8 +271,8 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -208,15 +300,15 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,58 +9,101 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630330987096';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -68,8 +112,8 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -78,12 +122,11 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
@ -92,77 +135,127 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
// @ts-ignore
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -172,7 +265,8 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -181,10 +275,9 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
|
@ -200,15 +293,15 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ export class Telemetry {
|
|||
this.client.identify(
|
||||
{
|
||||
userId: this.instanceId,
|
||||
anonymousId: '000000000000',
|
||||
traits: {
|
||||
...traits,
|
||||
instanceId: this.instanceId,
|
||||
|
@ -138,6 +139,7 @@ export class Telemetry {
|
|||
this.client.track(
|
||||
{
|
||||
userId: this.instanceId,
|
||||
anonymousId: '000000000000',
|
||||
event: eventName,
|
||||
properties,
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "0.90.0",
|
||||
"version": "0.93.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -50,7 +50,7 @@
|
|||
"form-data": "^4.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-workflow": "~0.76.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"qs": "^6.10.1",
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
ICredentialsExpressionResolveValues,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteSingleFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestOptions,
|
||||
|
@ -71,7 +72,7 @@ import { fromBuffer } from 'file-type';
|
|||
import { lookup } from 'mime-types';
|
||||
|
||||
import axios, { AxiosProxyConfig, AxiosRequestConfig, Method } from 'axios';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { URL, URLSearchParams } from 'url';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
|
@ -86,6 +87,14 @@ import {
|
|||
axios.defaults.timeout = 300000;
|
||||
// Prevent axios from adding x-form-www-urlencoded headers by default
|
||||
axios.defaults.headers.post = {};
|
||||
axios.defaults.headers.put = {};
|
||||
axios.defaults.headers.patch = {};
|
||||
axios.defaults.paramsSerializer = (params) => {
|
||||
if (params instanceof URLSearchParams) {
|
||||
return params.toString();
|
||||
}
|
||||
return stringify(params, { arrayFormat: 'indices' });
|
||||
};
|
||||
|
||||
const requestPromiseWithDefaults = requestPromise.defaults({
|
||||
timeout: 300000, // 5 minutes
|
||||
|
@ -128,6 +137,28 @@ function searchForHeader(headers: IDataObject, headerName: string) {
|
|||
return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName);
|
||||
}
|
||||
|
||||
async function generateContentLengthHeader(formData: FormData, headers: IDataObject) {
|
||||
if (!formData || !formData.getLength) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const length = await new Promise((res, rej) => {
|
||||
formData.getLength((error: Error | null, length: number) => {
|
||||
if (error) {
|
||||
rej(error);
|
||||
return;
|
||||
}
|
||||
res(length);
|
||||
});
|
||||
});
|
||||
headers = Object.assign(headers, {
|
||||
'content-length': length,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Unable to calculate form data length', { error });
|
||||
}
|
||||
}
|
||||
|
||||
async function parseRequestObject(requestObject: IDataObject) {
|
||||
// This function is a temporary implementation
|
||||
// That translates all http requests done via
|
||||
|
@ -192,6 +223,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
delete axiosConfig.headers[contentTypeHeaderKeyName];
|
||||
const headers = axiosConfig.data.getHeaders();
|
||||
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
|
||||
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
|
||||
} else {
|
||||
// When using the `form` property it means the content should be x-www-form-urlencoded.
|
||||
if (requestObject.form !== undefined && requestObject.body === undefined) {
|
||||
|
@ -228,6 +260,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
// Mix in headers as FormData creates the boundary.
|
||||
const headers = axiosConfig.data.getHeaders();
|
||||
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
|
||||
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
|
||||
} else if (requestObject.body !== undefined) {
|
||||
// If we have body and possibly form
|
||||
if (requestObject.form !== undefined) {
|
||||
|
@ -338,7 +371,63 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
}
|
||||
|
||||
if (requestObject.proxy !== undefined) {
|
||||
axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig;
|
||||
// try our best to parse the url provided.
|
||||
if (typeof requestObject.proxy === 'string') {
|
||||
try {
|
||||
const url = new URL(requestObject.proxy);
|
||||
axiosConfig.proxy = {
|
||||
host: url.hostname,
|
||||
port: parseInt(url.port, 10),
|
||||
protocol: url.protocol,
|
||||
};
|
||||
if (!url.port) {
|
||||
// Sets port to a default if not informed
|
||||
if (url.protocol === 'http') {
|
||||
axiosConfig.proxy.port = 80;
|
||||
} else if (url.protocol === 'https') {
|
||||
axiosConfig.proxy.port = 443;
|
||||
}
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
axiosConfig.proxy.auth = {
|
||||
username: url.username,
|
||||
password: url.password,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Not a valid URL. We will try to simply parse stuff
|
||||
// such as user:pass@host:port without protocol (we'll assume http)
|
||||
if (requestObject.proxy.includes('@')) {
|
||||
const [userpass, hostport] = requestObject.proxy.split('@');
|
||||
const [username, password] = userpass.split(':');
|
||||
const [hostname, port] = hostport.split(':');
|
||||
axiosConfig.proxy = {
|
||||
host: hostname,
|
||||
port: parseInt(port, 10),
|
||||
protocol: 'http',
|
||||
auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (requestObject.proxy.includes(':')) {
|
||||
const [hostname, port] = requestObject.proxy.split(':');
|
||||
axiosConfig.proxy = {
|
||||
host: hostname,
|
||||
port: parseInt(port, 10),
|
||||
protocol: 'http',
|
||||
};
|
||||
} else {
|
||||
axiosConfig.proxy = {
|
||||
host: requestObject.proxy,
|
||||
port: 80,
|
||||
protocol: 'http',
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestObject.encoding === null) {
|
||||
|
@ -357,6 +446,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
if (
|
||||
requestObject.json !== false &&
|
||||
axiosConfig.data !== undefined &&
|
||||
axiosConfig.data !== '' &&
|
||||
!(axiosConfig.data instanceof Buffer) &&
|
||||
!allHeaders.some((headerKey) => headerKey.toLowerCase() === 'content-type')
|
||||
) {
|
||||
|
@ -406,6 +496,11 @@ async function proxyRequestToAxios(
|
|||
|
||||
axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject));
|
||||
|
||||
Logger.debug('Proxying request to axios', {
|
||||
originalConfig: configObject,
|
||||
parsedConfig: axiosConfig,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
|
@ -1567,6 +1662,9 @@ export function getExecuteFunctions(
|
|||
Logger.warn(`There was a problem sending messsage to UI: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
|
||||
await additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
|
||||
},
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
|
|
|
@ -12,7 +12,6 @@ export * from './ActiveWorkflows';
|
|||
export * from './ActiveWebhooks';
|
||||
export * from './Constants';
|
||||
export * from './Credentials';
|
||||
export * from './DeferredPromise';
|
||||
export * from './Interfaces';
|
||||
export * from './LoadNodeParameterOptions';
|
||||
export * from './NodeExecuteFunctions';
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
ICredentialDataDecryptedObject,
|
||||
ICredentialsHelper,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteWorkflowInfo,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
|
@ -20,7 +21,7 @@ import {
|
|||
WorkflowHooks,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
|
||||
import { Credentials, IExecuteFunctions } from '../src';
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
getDecrypted(
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { IConnections, ILogger, INode, IRun, LoggerProxy, Workflow } from 'n8n-workflow';
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IConnections,
|
||||
ILogger,
|
||||
INode,
|
||||
IRun,
|
||||
LoggerProxy,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { createDeferredPromise, WorkflowExecute } from '../src';
|
||||
import { WorkflowExecute } from '../src';
|
||||
|
||||
import * as Helpers from './Helpers';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
|
|
|
@ -16,7 +16,7 @@ import VariableTable from './VariableTable.vue';
|
|||
<Canvas>
|
||||
<Story name="border-radius">
|
||||
{{
|
||||
template: `<variable-table :variables="['--border-radius-small','--border-radius-base']" />`,
|
||||
template: `<variable-table :variables="['--border-radius-small','--border-radius-base', '--border-radius-large', '--border-radius-xlarge']" />`,
|
||||
components: {
|
||||
VariableTable,
|
||||
},
|
||||
|
|
|
@ -44,7 +44,7 @@ import ColorCircles from './ColorCircles.vue';
|
|||
<Canvas>
|
||||
<Story name="success">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2']" />`,
|
||||
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2', '--color-success-light']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
|
@ -109,7 +109,7 @@ import ColorCircles from './ColorCircles.vue';
|
|||
<Canvas>
|
||||
<Story name="foreground">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
||||
template: `<color-circles :colors="['--color-foreground-xdark', '--color-foreground-dark', '--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
|
@ -129,3 +129,16 @@ import ColorCircles from './ColorCircles.vue';
|
|||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Canvas
|
||||
|
||||
<Canvas>
|
||||
<Story name="canvas">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-canvas-background', '--color-canvas-dot']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
|
|
@ -75,6 +75,15 @@
|
|||
var(--color-success-tint-2-l)
|
||||
);
|
||||
|
||||
--color-success-light-h: 150;
|
||||
--color-success-light-s: 54%;
|
||||
--color-success-light-l: 70%;
|
||||
--color-success-light: hsl(
|
||||
var(--color-success-light-h),
|
||||
var(--color-success-light-s),
|
||||
var(--color-success-light-l)
|
||||
);
|
||||
|
||||
--color-warning-h: 36;
|
||||
--color-warning-s: 77%;
|
||||
--color-warning-l: 57%;
|
||||
|
@ -187,6 +196,24 @@
|
|||
var(--color-text-xlight-l)
|
||||
);
|
||||
|
||||
--color-foreground-xdark-h: 220;
|
||||
--color-foreground-xdark-s: 7.4%;
|
||||
--color-foreground-xdark-l: 52.5%;
|
||||
--color-foreground-xdark: hsl(
|
||||
var(--color-foreground-xdark-h),
|
||||
var(--color-foreground-xdark-s),
|
||||
var(--color-foreground-xdark-l)
|
||||
);
|
||||
|
||||
--color-foreground-dark-h: 228;
|
||||
--color-foreground-dark-s: 9.6%;
|
||||
--color-foreground-dark-l: 79.6%;
|
||||
--color-foreground-dark: hsl(
|
||||
var(--color-foreground-dark-h),
|
||||
var(--color-foreground-dark-s),
|
||||
var(--color-foreground-dark-l)
|
||||
);
|
||||
|
||||
--color-foreground-base-h: 220;
|
||||
--color-foreground-base-s: 20%;
|
||||
--color-foreground-base-l: 88.2%;
|
||||
|
@ -259,6 +286,25 @@
|
|||
var(--color-background-xlight-l)
|
||||
);
|
||||
|
||||
--color-canvas-dot-h: 204;
|
||||
--color-canvas-dot-s: 15.6%;
|
||||
--color-canvas-dot-l: 87.5%;
|
||||
--color-canvas-dot: hsl(
|
||||
var(--color-canvas-dot-h),
|
||||
var(--color-canvas-dot-s),
|
||||
var(--color-canvas-dot-l)
|
||||
);
|
||||
|
||||
--color-canvas-background-h: 260;
|
||||
--color-canvas-background-s: 100%;
|
||||
--color-canvas-background-l: 99.4%;
|
||||
--color-canvas-background: hsl(
|
||||
var(--color-canvas-background-h),
|
||||
var(--color-canvas-background-s),
|
||||
var(--color-canvas-background-l)
|
||||
);
|
||||
|
||||
--border-radius-xlarge: 12px;
|
||||
--border-radius-large: 8px;
|
||||
--border-radius-base: 4px;
|
||||
--border-radius-small: 2px;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.113.0",
|
||||
"version": "0.116.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -26,7 +26,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fontsource/open-sans": "^4.5.0",
|
||||
"n8n-design-system": "~0.5.0",
|
||||
"n8n-design-system": "~0.6.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-fragment": "^1.5.2"
|
||||
|
@ -71,7 +71,7 @@
|
|||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-workflow": "~0.76.0",
|
||||
"sass": "^1.26.5",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"prismjs": "^1.17.1",
|
||||
|
|
|
@ -22,32 +22,61 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
PaintStyle,
|
||||
} from 'jsplumb';
|
||||
|
||||
declare module 'jsplumb' {
|
||||
interface PaintStyle {
|
||||
stroke?: string;
|
||||
fill?: string;
|
||||
strokeWidth?: number;
|
||||
outlineStroke?: string;
|
||||
outlineWidth?: number;
|
||||
}
|
||||
|
||||
interface Anchor {
|
||||
lastReturnValue: number[];
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
__meta?: {
|
||||
sourceNodeName: string,
|
||||
sourceOutputIndex: number,
|
||||
targetNodeName: string,
|
||||
targetOutputIndex: number,
|
||||
};
|
||||
canvas?: HTMLElement;
|
||||
connector?: {
|
||||
setTargetEndpoint: (endpoint: Endpoint) => void;
|
||||
resetTargetEndpoint: () => void;
|
||||
bounds: {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
||||
};
|
||||
|
||||
// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
|
||||
bind(event: string, callback: Function): void; // tslint:disable-line:no-any
|
||||
bind(event: string, callback: Function): void;
|
||||
removeOverlay(name: string): void;
|
||||
removeOverlays(): void;
|
||||
setParameter(name: string, value: any): void; // tslint:disable-line:no-any
|
||||
setPaintStyle(arg0: PaintStyle): void;
|
||||
addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
|
||||
setConnector(arg0: any[]): void; // tslint:disable-line:no-any
|
||||
getUuids(): [string, string];
|
||||
}
|
||||
|
||||
interface Endpoint {
|
||||
__meta?: {
|
||||
nodeName: string,
|
||||
index: number,
|
||||
};
|
||||
getOverlay(name: string): any; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
interface Overlay {
|
||||
setVisible(visible: boolean): void;
|
||||
setLocation(location: number): void;
|
||||
canvas?: HTMLElement;
|
||||
}
|
||||
|
||||
interface OnConnectionBindInfo {
|
||||
|
@ -66,18 +95,14 @@ export interface IEndpointOptions {
|
|||
dragProxy?: any; // tslint:disable-line:no-any
|
||||
endpoint?: string;
|
||||
endpointStyle?: object;
|
||||
endpointHoverStyle?: object;
|
||||
isSource?: boolean;
|
||||
isTarget?: boolean;
|
||||
maxConnections?: number;
|
||||
overlays?: any; // tslint:disable-line:no-any
|
||||
parameters?: any; // tslint:disable-line:no-any
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export interface IConnectionsUi {
|
||||
[key: string]: {
|
||||
[key: string]: IEndpointOptions;
|
||||
};
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpdateInformation {
|
||||
|
@ -95,20 +120,16 @@ export interface INodeUpdatePropertiesInformation {
|
|||
};
|
||||
}
|
||||
|
||||
export type XYPositon = [number, number];
|
||||
export type XYPosition = [number, number];
|
||||
|
||||
export type MessageType = 'success' | 'warning' | 'info' | 'error';
|
||||
|
||||
export interface INodeUi extends INode {
|
||||
position: XYPositon;
|
||||
position: XYPosition;
|
||||
color?: string;
|
||||
notes?: string;
|
||||
issues?: INodeIssues;
|
||||
_jsPlumb?: {
|
||||
endpoints?: {
|
||||
[key: string]: IEndpointOptions[];
|
||||
};
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface INodeTypesMaxCount {
|
||||
|
@ -604,7 +625,7 @@ export interface IRootState {
|
|||
lastSelectedNodeOutputIndex: number | null;
|
||||
nodeIndex: Array<string | null>;
|
||||
nodeTypes: INodeTypeDescription[];
|
||||
nodeViewOffsetPosition: XYPositon;
|
||||
nodeViewOffsetPosition: XYPosition;
|
||||
nodeViewMoveInProgress: boolean;
|
||||
selectedNodes: INodeUi[];
|
||||
sessionId: string;
|
||||
|
@ -670,5 +691,13 @@ export interface IRestApiContext {
|
|||
|
||||
export interface IZoomConfig {
|
||||
scale: number;
|
||||
offset: XYPositon;
|
||||
offset: XYPosition;
|
||||
}
|
||||
|
||||
export interface IBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,35 @@
|
|||
<template>
|
||||
<div class="node-wrapper" :style="nodePosition">
|
||||
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
|
||||
<div v-if="hasIssues" class="node-info-icon node-issues">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="nodeIssues"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<el-badge v-else :hidden="workflowDataItems === 0" class="node-info-icon data-count" :value="workflowDataItems"></el-badge>
|
||||
<div :class="{'node-wrapper': true, selected: isSelected}" :style="nodePosition">
|
||||
<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">
|
||||
<div v-if="!data.disabled" :class="{'node-info-icon': true, 'shift-icon': shiftOutputCount}">
|
||||
<div v-if="hasIssues" class="node-issues">
|
||||
<n8n-tooltip placement="bottom" >
|
||||
<div slot="content" v-html="nodeIssues"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div v-else-if="waiting" class="waiting">
|
||||
<n8n-tooltip placement="bottom">
|
||||
<div slot="content" v-html="waiting"></div>
|
||||
<font-awesome-icon icon="clock" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<span v-else-if="workflowDataItems" class="data-count">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span v-if="workflowDataItems > 1" class="items-count"> {{ workflowDataItems }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="waiting" class="node-info-icon waiting">
|
||||
<n8n-tooltip placement="top">
|
||||
<div slot="content" v-html="waiting"></div>
|
||||
<font-awesome-icon icon="clock" />
|
||||
</n8n-tooltip>
|
||||
<div class="node-executing-info" title="Node is executing">
|
||||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" :disabled="this.data.disabled"/>
|
||||
</div>
|
||||
|
||||
<div class="node-executing-info" title="Node is executing">
|
||||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly">
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
|
||||
<div v-touch:tap="deleteNode" class="option" title="Delete Node" >
|
||||
<font-awesome-icon icon="trash" />
|
||||
</div>
|
||||
|
@ -36,12 +46,12 @@
|
|||
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :circle="true" :shrink="true" :disabled="this.data.disabled"/>
|
||||
<div :class="{'disabled-linethrough': true, success: workflowDataItems > 0}" v-if="showDisabledLinethrough"></div>
|
||||
</div>
|
||||
<div class="node-description">
|
||||
<div class="node-name" :title="data.name">
|
||||
{{data.name}}
|
||||
<p>{{ nodeTitle }}</p>
|
||||
<p v-if="data.disabled">(Disabled)</p>
|
||||
</div>
|
||||
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
|
||||
{{nodeSubtitle}}
|
||||
|
@ -61,6 +71,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
|||
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
ITaskData,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -69,6 +80,8 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { getStyleTokenValue } from './helpers';
|
||||
import { INodeUi, XYPosition } from '@/Interface';
|
||||
|
||||
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||
name: 'Node',
|
||||
|
@ -76,8 +89,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
NodeIcon,
|
||||
},
|
||||
computed: {
|
||||
workflowDataItems () {
|
||||
const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
||||
nodeRunData(): ITaskData[] {
|
||||
return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
||||
},
|
||||
hasIssues (): boolean {
|
||||
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
workflowDataItems (): number {
|
||||
const workflowResultDataNode = this.nodeRunData;
|
||||
if (workflowResultDataNode === null) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -90,34 +112,12 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
nodeType (): INodeTypeDescription | null {
|
||||
return this.$store.getters.nodeType(this.data.type);
|
||||
},
|
||||
nodeClass () {
|
||||
const classes = [];
|
||||
|
||||
if (this.data.disabled) {
|
||||
classes.push('disabled');
|
||||
}
|
||||
|
||||
if (this.isExecuting) {
|
||||
classes.push('executing');
|
||||
}
|
||||
|
||||
if (this.workflowDataItems !== 0) {
|
||||
classes.push('has-data');
|
||||
}
|
||||
|
||||
if (this.hasIssues) {
|
||||
classes.push('has-issues');
|
||||
}
|
||||
|
||||
if (this.isTouchDevice) {
|
||||
classes.push('is-touch-device');
|
||||
}
|
||||
|
||||
if (this.isTouchActive) {
|
||||
classes.push('touch-active');
|
||||
}
|
||||
|
||||
return classes;
|
||||
nodeClass (): object {
|
||||
return {
|
||||
'node-box': true,
|
||||
disabled: this.data.disabled,
|
||||
executing: this.isExecuting,
|
||||
};
|
||||
},
|
||||
nodeIssues (): string {
|
||||
if (this.data.issues === undefined) {
|
||||
|
@ -135,6 +135,27 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
return 'play';
|
||||
}
|
||||
},
|
||||
position (): XYPosition {
|
||||
const node = this.$store.getters.nodesByName[this.name] as INodeUi; // position responsive to store changes
|
||||
|
||||
return node.position;
|
||||
},
|
||||
showDisabledLinethrough(): boolean {
|
||||
return !!(this.data.disabled && this.nodeType && this.nodeType.inputs.length === 1 && this.nodeType.outputs.length === 1);
|
||||
},
|
||||
nodePosition (): object {
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
left: this.position[0] + 'px',
|
||||
top: this.position[1] + 'px',
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
nodeTitle (): string {
|
||||
return this.data.name;
|
||||
},
|
||||
waiting (): string | undefined {
|
||||
const workflowExecution = this.$store.getters.getWorkflowExecution;
|
||||
|
||||
|
@ -154,6 +175,38 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
workflowRunning (): boolean {
|
||||
return this.$store.getters.isActionActive('workflowRunning');
|
||||
},
|
||||
nodeStyle (): object {
|
||||
let borderColor = getStyleTokenValue('--color-foreground-xdark');
|
||||
|
||||
if (this.data.disabled) {
|
||||
borderColor = getStyleTokenValue('--color-foreground-base');
|
||||
}
|
||||
else if (!this.isExecuting) {
|
||||
if (this.hasIssues) {
|
||||
borderColor = getStyleTokenValue('--color-danger');
|
||||
}
|
||||
else if (this.waiting) {
|
||||
borderColor = getStyleTokenValue('--color-secondary');
|
||||
}
|
||||
else if (this.workflowDataItems) {
|
||||
borderColor = getStyleTokenValue('--color-success');
|
||||
}
|
||||
}
|
||||
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
'border-color': borderColor,
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
isSelected (): boolean {
|
||||
return this.$store.getters.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name);
|
||||
},
|
||||
shiftOutputCount (): boolean {
|
||||
return !!(this.nodeType && this.nodeType.outputs.length > 2);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isActive(newValue, oldValue) {
|
||||
|
@ -161,9 +214,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
this.setSubtitle();
|
||||
}
|
||||
},
|
||||
nodeRunData(newValue) {
|
||||
this.$emit('run', {name: this.data.name, data: newValue, waiting: !!this.waiting});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSubtitle();
|
||||
setTimeout(() => {
|
||||
this.$emit('run', {name: this.data.name, data: this.nodeRunData, waiting: !!this.waiting});
|
||||
}, 0);
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -213,7 +272,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.node-wrapper {
|
||||
position: absolute;
|
||||
|
@ -221,20 +280,25 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
height: 100px;
|
||||
|
||||
.node-description {
|
||||
line-height: 1.5;
|
||||
position: absolute;
|
||||
bottom: -55px;
|
||||
top: 100px;
|
||||
left: -50px;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
padding: 8px;
|
||||
width: 200px;
|
||||
pointer-events: none; // prevent container from being draggable
|
||||
|
||||
.node-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
.node-name > p { // must be paragraph tag to have two lines in safari
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--font-line-height-compact);
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
|
@ -248,33 +312,24 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
}
|
||||
|
||||
.node-default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
z-index: 24;
|
||||
cursor: pointer;
|
||||
color: #444;
|
||||
border: 1px dashed grey;
|
||||
|
||||
&.has-data {
|
||||
border-style: solid;
|
||||
}
|
||||
.node-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: var(--color-background-xlight);
|
||||
|
||||
&.disabled {
|
||||
color: #a0a0a0;
|
||||
text-decoration: line-through;
|
||||
border: 1px solid #eee !important;
|
||||
background-color: #eee;
|
||||
}
|
||||
&.executing {
|
||||
background-color: $--color-primary-light !important;
|
||||
|
||||
&.executing {
|
||||
background-color: $--color-primary-light !important;
|
||||
border-color: $--color-primary !important;
|
||||
|
||||
.node-executing-info {
|
||||
display: inline-block;
|
||||
.node-executing-info {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,39 +360,35 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
.node-icon {
|
||||
position: absolute;
|
||||
top: calc(50% - 30px);
|
||||
left: calc(50% - 30px);
|
||||
top: calc(50% - 20px);
|
||||
left: calc(50% - 20px);
|
||||
}
|
||||
|
||||
.node-info-icon {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
right: 12px;
|
||||
z-index: 11;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
|
||||
&.data-count {
|
||||
&.shift-icon {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.data-count {
|
||||
font-weight: 600;
|
||||
top: -12px;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
left: 10px;
|
||||
top: -12px;
|
||||
.node-issues {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.node-issues {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 20px;
|
||||
color: #ff0000;
|
||||
.items-count {
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
|
||||
.waiting {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 20px;
|
||||
color: #5e5efa;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.node-options {
|
||||
|
@ -346,7 +397,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
top: -25px;
|
||||
left: -10px;
|
||||
width: 120px;
|
||||
height: 45px;
|
||||
height: 24px;
|
||||
font-size: 0.9em;
|
||||
text-align: left;
|
||||
z-index: 10;
|
||||
|
@ -381,45 +432,94 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-data .node-options,
|
||||
&.has-issues .node-options {
|
||||
top: -35px;
|
||||
}
|
||||
.select-background {
|
||||
display: block;
|
||||
background-color: hsla(var(--color-foreground-base-h), var(--color-foreground-base-s), var(--color-foreground-base-l), 60%);
|
||||
border-radius: var(--border-radius-xlarge);
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: -8px !important;
|
||||
top: -8px !important;
|
||||
height: 116px;
|
||||
width: 116px !important;
|
||||
}
|
||||
|
||||
.disabled-linethrough {
|
||||
border: 1px solid var(--color-foreground-dark);
|
||||
position: absolute;
|
||||
top: 49px;
|
||||
left: -3px;
|
||||
width: 111px;
|
||||
pointer-events: none;
|
||||
|
||||
&.success {
|
||||
border-color: var(--color-success-light);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.el-badge__content {
|
||||
border-width: 2px;
|
||||
background-color: #67c23a;
|
||||
<style lang="scss">
|
||||
/** node */
|
||||
.node-wrapper.selected {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/** connector */
|
||||
.jtk-connector {
|
||||
z-index:4;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.jtk-connector path {
|
||||
transition: stroke .1s ease-in-out;
|
||||
}
|
||||
|
||||
.jtk-connector.success {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/** node endpoints */
|
||||
.jtk-endpoint {
|
||||
z-index:5;
|
||||
}
|
||||
|
||||
.jtk-connector.jtk-hover {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.disabled-linethrough {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.jtk-endpoint.jtk-hover {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.jtk-overlay {
|
||||
z-index:6;
|
||||
z-index:7;
|
||||
}
|
||||
|
||||
.jtk-endpoint.dropHover {
|
||||
border: 2px solid #ff2244;
|
||||
.jtk-connector.jtk-dragging {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.jtk-drag-selected .node-default {
|
||||
/* https://www.cssmatic.com/box-shadow */
|
||||
-webkit-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
-moz-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
.jtk-endpoint.jtk-drag-active {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.disabled .node-icon img {
|
||||
-webkit-filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
.connection-actions {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.node-options {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.drop-add-node-label {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -101,7 +101,7 @@ export default mixins(
|
|||
credentialTypesNodeDescription (): INodeCredentialDescription[] {
|
||||
const node = this.node as INodeUi;
|
||||
|
||||
const activeNodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription;
|
||||
const activeNodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
|
||||
if (activeNodeType && activeNodeType.credentials) {
|
||||
return activeNodeType.credentials;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
|
||||
<div class="node-icon-wrapper" :style="iconStyleData">
|
||||
<div v-if="nodeIconData !== null" class="icon">
|
||||
<img v-if="nodeIconData.type === 'file'" :src="nodeIconData.fileBuffer || nodeIconData.path" style="max-width: 100%; max-height: 100%;" />
|
||||
<font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" />
|
||||
<img v-if="nodeIconData.type === 'file'" :src="nodeIconData.fileBuffer || nodeIconData.path" :style="imageStyleData" />
|
||||
<font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" :style="fontStyleData" />
|
||||
</div>
|
||||
<div v-else class="node-icon-placeholder">
|
||||
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
|
@ -12,39 +12,65 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
import { IVersionNode } from '@/Interface';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
|
||||
interface NodeIconData {
|
||||
type: string;
|
||||
path: string;
|
||||
path?: string;
|
||||
fileExtension?: string;
|
||||
fileBuffer?: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'NodeIcon',
|
||||
props: [
|
||||
'nodeType',
|
||||
'size',
|
||||
'shrink',
|
||||
'disabled',
|
||||
'circle',
|
||||
],
|
||||
props: {
|
||||
nodeType: {},
|
||||
size: {
|
||||
type: Number,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconStyleData (): object {
|
||||
const color = this.disabled ? '#ccc' : this.nodeType.defaults && this.nodeType.defaults.color;
|
||||
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
|
||||
const color = nodeType ? nodeType.defaults && nodeType!.defaults.color : '';
|
||||
if (!this.size) {
|
||||
return {color};
|
||||
}
|
||||
|
||||
const size = parseInt(this.size, 10);
|
||||
|
||||
return {
|
||||
color,
|
||||
width: size + 'px',
|
||||
height: size + 'px',
|
||||
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
|
||||
'line-height': size + 'px',
|
||||
'border-radius': this.circle ? '50%': '4px',
|
||||
width: this.size + 'px',
|
||||
height: this.size + 'px',
|
||||
'font-size': this.size + 'px',
|
||||
'line-height': this.size + 'px',
|
||||
'border-radius': this.circle ? '50%': '2px',
|
||||
...(this.disabled && {
|
||||
color: '#ccc',
|
||||
'-webkit-filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
'filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
}),
|
||||
};
|
||||
},
|
||||
fontStyleData (): object {
|
||||
return {
|
||||
'max-width': this.size + 'px',
|
||||
};
|
||||
},
|
||||
imageStyleData (): object {
|
||||
return {
|
||||
width: '100%',
|
||||
'max-width': '100%',
|
||||
'max-height': '100%',
|
||||
};
|
||||
},
|
||||
isSvgIcon (): boolean {
|
||||
|
@ -54,26 +80,27 @@ export default Vue.extend({
|
|||
return false;
|
||||
},
|
||||
nodeIconData (): null | NodeIconData {
|
||||
if (this.nodeType === null) {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
|
||||
if (nodeType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.nodeType.iconData) {
|
||||
return this.nodeType.iconData;
|
||||
if ((nodeType as IVersionNode).iconData) {
|
||||
return (nodeType as IVersionNode).iconData;
|
||||
}
|
||||
|
||||
const restUrl = this.$store.getters.getRestUrl;
|
||||
|
||||
if (this.nodeType.icon) {
|
||||
if (nodeType.icon) {
|
||||
let type, path;
|
||||
[type, path] = this.nodeType.icon.split(':');
|
||||
[type, path] = nodeType.icon.split(':');
|
||||
const returnData: NodeIconData = {
|
||||
type,
|
||||
path,
|
||||
};
|
||||
|
||||
if (type === 'file') {
|
||||
returnData.path = restUrl + '/node-icon/' + this.nodeType.name;
|
||||
returnData.path = restUrl + '/node-icon/' + nodeType.name;
|
||||
returnData.fileExtension = path.split('.').slice(-1).join();
|
||||
}
|
||||
|
||||
|
@ -90,7 +117,7 @@ export default Vue.extend({
|
|||
.node-icon-wrapper {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
color: #444;
|
||||
line-height: 26px;
|
||||
font-size: 1.1em;
|
||||
|
@ -99,7 +126,7 @@ export default Vue.extend({
|
|||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
|
||||
&.full .icon {
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
|
@ -108,10 +135,6 @@ export default Vue.extend({
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
&.shrink .icon {
|
||||
margin: 0.24em;
|
||||
}
|
||||
|
||||
.node-icon-placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -122,13 +122,6 @@ export default mixins(
|
|||
|
||||
return this.nodeType.properties;
|
||||
},
|
||||
isColorDefaultValue (): boolean {
|
||||
if (this.nodeType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.node.color === this.nodeType.defaults.color;
|
||||
},
|
||||
workflowRunning (): boolean {
|
||||
return this.$store.getters.isActionActive('workflowRunning');
|
||||
},
|
||||
|
@ -170,14 +163,6 @@ export default mixins(
|
|||
noDataExpression: true,
|
||||
description: 'If active, the note above will display in the flow as a subtitle.',
|
||||
},
|
||||
{
|
||||
displayName: 'Node Color',
|
||||
name: 'color',
|
||||
type: 'color',
|
||||
default: '#ff0000',
|
||||
noDataExpression: true,
|
||||
description: 'The color of the node in the flow.',
|
||||
},
|
||||
{
|
||||
displayName: 'Always Output Data',
|
||||
name: 'alwaysOutputData',
|
||||
|
@ -317,7 +302,7 @@ export default mixins(
|
|||
// Update the values on the node
|
||||
this.$store.commit('updateNodeProperties', updateInformation);
|
||||
|
||||
const node = this.$store.getters.nodeByName(updateInformation.name);
|
||||
const node = this.$store.getters.getNodeByName(updateInformation.name);
|
||||
|
||||
// Update the issues
|
||||
this.updateNodeCredentialIssues(node);
|
||||
|
@ -337,7 +322,7 @@ export default mixins(
|
|||
// Save the node name before we commit the change because
|
||||
// we need the old name to rename the node properly
|
||||
const nodeNameBefore = parameterData.node || this.node.name;
|
||||
const node = this.$store.getters.nodeByName(nodeNameBefore);
|
||||
const node = this.$store.getters.getNodeByName(nodeNameBefore);
|
||||
if (parameterData.name === 'name') {
|
||||
// Name of node changed so we have to set also the new node name as active
|
||||
|
||||
|
@ -353,7 +338,10 @@ export default mixins(
|
|||
} else if (parameterData.name.startsWith('parameters.')) {
|
||||
// A node parameter changed
|
||||
|
||||
const nodeType = this.$store.getters.nodeType(node.type);
|
||||
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
|
||||
if (!nodeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get only the parameters which are different to the defaults
|
||||
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
|
||||
|
@ -491,10 +479,6 @@ export default mixins(
|
|||
// Set default value
|
||||
Vue.set(this.nodeValues, nodeSetting.name, nodeSetting.default);
|
||||
}
|
||||
if (nodeSetting.name === 'color') {
|
||||
// For color also apply the default node color to the node settings
|
||||
nodeSetting.default = this.nodeType.defaults.color;
|
||||
}
|
||||
}
|
||||
|
||||
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));
|
||||
|
|
|
@ -64,7 +64,7 @@ export default mixins(
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
isMinimized: this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
||||
isMinimized: this.nodeType && this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
||||
showUrlFor: 'test',
|
||||
};
|
||||
},
|
||||
|
|
|
@ -203,6 +203,7 @@ import {
|
|||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeTypeDescription,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
|
@ -529,8 +530,8 @@ export default mixins(
|
|||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
const nodeType = this.$store.getters.nodeType(this.node.type);
|
||||
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
|
||||
const nodeType = this.$store.getters.nodeType(this.node.type) as INodeTypeDescription | null;
|
||||
if (!nodeType || !nodeType.outputNames || nodeType.outputNames.length <= outputIndex) {
|
||||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,3 +13,8 @@ export function convertToHumanReadableDate (epochTime: number) {
|
|||
export function getAppNameFromCredType(name: string) {
|
||||
return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
||||
}
|
||||
|
||||
export function getStyleTokenValue(name: string): string {
|
||||
const style = getComputedStyle(document.body);
|
||||
return style.getPropertyValue(name);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { INodeUi } from '@/Interface';
|
||||
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,
|
||||
|
@ -42,23 +43,14 @@ export const mouseSelect = mixins(
|
|||
}
|
||||
return e.ctrlKey;
|
||||
},
|
||||
/**
|
||||
* Gets mouse position within the node view. Both node view offset and scale (zoom) are considered when
|
||||
* calculating position.
|
||||
*
|
||||
* @param event - mouse event within node view
|
||||
*/
|
||||
getMousePositionWithinNodeView (event: MouseEvent) {
|
||||
getMousePositionWithinNodeView (event: MouseEvent | TouchEvent): XYPosition {
|
||||
const [x, y] = getMousePosition(event);
|
||||
// @ts-ignore
|
||||
const nodeViewScale = this.nodeViewScale;
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
return {
|
||||
x: (event.pageX - offsetPosition[0]) / nodeViewScale,
|
||||
y: (event.pageY - offsetPosition[1]) / nodeViewScale,
|
||||
};
|
||||
return getRelativePosition(x, y, this.nodeViewScale, this.$store.getters.getNodeViewOffsetPosition);
|
||||
},
|
||||
showSelectBox (event: MouseEvent) {
|
||||
this.selectBox = Object.assign(this.selectBox, this.getMousePositionWithinNodeView(event));
|
||||
const [x, y] = this.getMousePositionWithinNodeView(event);
|
||||
this.selectBox = Object.assign(this.selectBox, {x, y});
|
||||
|
||||
// @ts-ignore
|
||||
this.selectBox.style.left = this.selectBox.x + 'px';
|
||||
|
@ -90,7 +82,7 @@ export const mouseSelect = mixins(
|
|||
this.selectActive = false;
|
||||
},
|
||||
getSelectionBox (event: MouseEvent) {
|
||||
const {x, y} = this.getMousePositionWithinNodeView(event);
|
||||
const [x, y] = this.getMousePositionWithinNodeView(event);
|
||||
return {
|
||||
// @ts-ignore
|
||||
x: Math.min(x, this.selectBox.x),
|
||||
|
@ -162,6 +154,10 @@ export const mouseSelect = mixins(
|
|||
this.nodeSelected(node);
|
||||
});
|
||||
|
||||
if (selectedNodes.length === 1) {
|
||||
this.$store.commit('setLastSelectedNode', selectedNodes[0].name);
|
||||
}
|
||||
|
||||
this.hideSelectBox();
|
||||
},
|
||||
mouseMoveSelect (e: MouseEvent) {
|
||||
|
@ -195,6 +191,10 @@ export const mouseSelect = mixins(
|
|||
this.$store.commit('setLastSelectedNode', null);
|
||||
this.$store.commit('setLastSelectedNodeOutputIndex', null);
|
||||
this.$store.commit('setActiveNode', null);
|
||||
// @ts-ignore
|
||||
this.lastSelectedConnection = null;
|
||||
// @ts-ignore
|
||||
this.newNodeInsertPosition = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import mixins from 'vue-typed-mixins';
|
|||
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,
|
||||
|
@ -15,29 +16,18 @@ export const moveNodeWorkflow = mixins(
|
|||
},
|
||||
|
||||
methods: {
|
||||
getMousePosition(e: MouseEvent | TouchEvent) {
|
||||
// @ts-ignore
|
||||
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
|
||||
// @ts-ignore
|
||||
const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
},
|
||||
moveWorkflow (e: MouseEvent) {
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
|
||||
const position = this.getMousePosition(e);
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (x - this.moveLastPosition[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (y - this.moveLastPosition[1]);
|
||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY]});
|
||||
|
||||
// Update the last position
|
||||
this.moveLastPosition[0] = position.x;
|
||||
this.moveLastPosition[1] = position.y;
|
||||
this.moveLastPosition[0] = x;
|
||||
this.moveLastPosition[1] = y;
|
||||
},
|
||||
mouseDownMoveWorkflow (e: MouseEvent) {
|
||||
if (this.isCtrlKeyPressed(e) === false) {
|
||||
|
@ -53,10 +43,10 @@ export const moveNodeWorkflow = mixins(
|
|||
|
||||
this.$store.commit('setNodeViewMoveInProgress', true);
|
||||
|
||||
const position = this.getMousePosition(e);
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
this.moveLastPosition[0] = position.x;
|
||||
this.moveLastPosition[1] = position.y;
|
||||
this.moveLastPosition[0] = x;
|
||||
this.moveLastPosition[1] = y;
|
||||
|
||||
// @ts-ignore
|
||||
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interface';
|
||||
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 } from '@/constants';
|
||||
import * as CanvasHelpers from '@/views/canvasHelpers';
|
||||
import { Endpoint } from 'jsplumb';
|
||||
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const nodeBase = mixins(
|
||||
deviceSupportHelpers,
|
||||
|
@ -18,145 +24,31 @@ export const nodeBase = mixins(
|
|||
},
|
||||
computed: {
|
||||
data (): INodeUi {
|
||||
return this.$store.getters.nodeByName(this.name);
|
||||
return this.$store.getters.getNodeByName(this.name);
|
||||
},
|
||||
hasIssues (): boolean {
|
||||
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
nodeName (): string {
|
||||
nodeId (): string {
|
||||
return NODE_NAME_PREFIX + this.nodeIndex;
|
||||
},
|
||||
nodeIndex (): string {
|
||||
return this.$store.getters.getNodeIndex(this.data.name).toString();
|
||||
},
|
||||
nodePosition (): object {
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
left: this.data.position[0] + 'px',
|
||||
top: this.data.position[1] + 'px',
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
nodeStyle (): object {
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
'border-color': this.data.color as string,
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
},
|
||||
props: [
|
||||
'name',
|
||||
'nodeId',
|
||||
'instance',
|
||||
'isReadOnly',
|
||||
'isActive',
|
||||
'hideActions',
|
||||
],
|
||||
methods: {
|
||||
__addNode (node: INodeUi) {
|
||||
// TODO: Later move the node-connection definitions to a special file
|
||||
|
||||
let nodeTypeData = this.$store.getters.nodeType(node.type);
|
||||
|
||||
const nodeConnectors: IConnectionsUi = {
|
||||
main: {
|
||||
input: {
|
||||
uuid: '-input',
|
||||
maxConnections: -1,
|
||||
endpoint: 'Rectangle',
|
||||
endpointStyle: {
|
||||
width: nodeTypeData && nodeTypeData.outputs.length > 2 ? 9 : 10,
|
||||
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 24,
|
||||
fill: '#777',
|
||||
stroke: '#777',
|
||||
lineWidth: 0,
|
||||
},
|
||||
dragAllowedWhenFull: true,
|
||||
},
|
||||
output: {
|
||||
uuid: '-output',
|
||||
maxConnections: -1,
|
||||
endpoint: 'Dot',
|
||||
endpointStyle: {
|
||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 11,
|
||||
fill: '#555',
|
||||
outlineStroke: 'none',
|
||||
},
|
||||
dragAllowedWhenFull: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!nodeTypeData) {
|
||||
// If node type is not know use by default the base.noOp data to display it
|
||||
nodeTypeData = this.$store.getters.nodeType(NO_OP_NODE_TYPE);
|
||||
}
|
||||
|
||||
const anchorPositions: {
|
||||
[key: string]: {
|
||||
[key: number]: string[] | number[][];
|
||||
}
|
||||
} = {
|
||||
input: {
|
||||
1: [
|
||||
'Left',
|
||||
],
|
||||
2: [
|
||||
[0, 0.3, -1, 0],
|
||||
[0, 0.7, -1, 0],
|
||||
],
|
||||
3: [
|
||||
[0, 0.25, -1, 0],
|
||||
[0, 0.5, -1, 0],
|
||||
[0, 0.75, -1, 0],
|
||||
],
|
||||
4: [
|
||||
[0, 0.2, -1, 0],
|
||||
[0, 0.4, -1, 0],
|
||||
[0, 0.6, -1, 0],
|
||||
[0, 0.8, -1, 0],
|
||||
],
|
||||
},
|
||||
output: {
|
||||
1: [
|
||||
'Right',
|
||||
],
|
||||
2: [
|
||||
[1, 0.3, 1, 0],
|
||||
[1, 0.7, 1, 0],
|
||||
],
|
||||
3: [
|
||||
[1, 0.25, 1, 0],
|
||||
[1, 0.5, 1, 0],
|
||||
[1, 0.75, 1, 0],
|
||||
],
|
||||
4: [
|
||||
[1, 0.2, 1, 0],
|
||||
[1, 0.4, 1, 0],
|
||||
[1, 0.6, 1, 0],
|
||||
[1, 0.8, 1, 0],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||
// Add Inputs
|
||||
let index, inputData, anchorPosition;
|
||||
let newEndpointData: IEndpointOptions;
|
||||
let indexData: {
|
||||
let index;
|
||||
const indexData: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
nodeTypeData.inputs.forEach((inputName: string) => {
|
||||
// @ts-ignore
|
||||
inputData = nodeConnectors[inputName].input;
|
||||
|
||||
nodeTypeData.inputs.forEach((inputName: string, i: number) => {
|
||||
// Increment the index for inputs with current name
|
||||
if (indexData.hasOwnProperty(inputName)) {
|
||||
indexData[inputName]++;
|
||||
|
@ -166,14 +58,15 @@ export const nodeBase = mixins(
|
|||
index = indexData[inputName];
|
||||
|
||||
// Get the position of the anchor depending on how many it has
|
||||
anchorPosition = anchorPositions.input[nodeTypeData.inputs.length][index];
|
||||
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
|
||||
|
||||
newEndpointData = {
|
||||
uuid: `${this.nodeIndex}` + inputData.uuid + index,
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeIndex, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: inputData.maxConnections,
|
||||
endpoint: inputData.endpoint,
|
||||
endpointStyle: inputData.endpointStyle,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Rectangle',
|
||||
endpointStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||
endpointHoverStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||
isSource: false,
|
||||
isTarget: !this.isReadOnly,
|
||||
parameters: {
|
||||
|
@ -181,7 +74,8 @@ export const nodeBase = mixins(
|
|||
type: inputName,
|
||||
index,
|
||||
},
|
||||
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
|
||||
enabled: !this.isReadOnly,
|
||||
dragAllowedWhenFull: true,
|
||||
dropOptions: {
|
||||
tolerance: 'touch',
|
||||
hoverClass: 'dropHover',
|
||||
|
@ -191,19 +85,15 @@ export const nodeBase = mixins(
|
|||
if (nodeTypeData.inputNames) {
|
||||
// Apply input names if they got set
|
||||
newEndpointData.overlays = [
|
||||
['Label',
|
||||
{
|
||||
id: 'input-name-label',
|
||||
location: [-2, 0.5],
|
||||
label: nodeTypeData.inputNames[index],
|
||||
cssClass: 'node-input-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
CanvasHelpers.getInputNameOverlay(nodeTypeData.inputNames[index]),
|
||||
];
|
||||
}
|
||||
|
||||
this.instance.addEndpoint(this.nodeName, newEndpointData);
|
||||
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
|
||||
endpoint.__meta = {
|
||||
nodeName: node.name,
|
||||
index: i,
|
||||
};
|
||||
|
||||
// TODO: Activate again if it makes sense. Currently makes problems when removing
|
||||
// connection on which the input has a name. It does not get hidden because
|
||||
|
@ -213,15 +103,17 @@ export const nodeBase = mixins(
|
|||
|
||||
// if (index === 0 && inputName === 'main') {
|
||||
// // Make the first main-input the default one to connect to when connection gets dropped on node
|
||||
// this.instance.makeTarget(this.nodeName, newEndpointData);
|
||||
// this.instance.makeTarget(this.nodeId, newEndpointData);
|
||||
// }
|
||||
});
|
||||
},
|
||||
__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||
let index;
|
||||
const indexData: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
// Add Outputs
|
||||
indexData = {};
|
||||
nodeTypeData.outputs.forEach((inputName: string) => {
|
||||
inputData = nodeConnectors[inputName].output;
|
||||
|
||||
nodeTypeData.outputs.forEach((inputName: string, i: number) => {
|
||||
// Increment the index for outputs with current name
|
||||
if (indexData.hasOwnProperty(inputName)) {
|
||||
indexData[inputName]++;
|
||||
|
@ -231,49 +123,48 @@ export const nodeBase = mixins(
|
|||
index = indexData[inputName];
|
||||
|
||||
// Get the position of the anchor depending on how many it has
|
||||
anchorPosition = anchorPositions.output[nodeTypeData.outputs.length][index];
|
||||
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
|
||||
|
||||
newEndpointData = {
|
||||
uuid: `${this.nodeIndex}` + inputData.uuid + index,
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: inputData.maxConnections,
|
||||
endpoint: inputData.endpoint,
|
||||
endpointStyle: inputData.endpointStyle,
|
||||
isSource: !this.isReadOnly,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Dot',
|
||||
endpointStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||
endpointHoverStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||
isSource: true,
|
||||
isTarget: false,
|
||||
enabled: !this.isReadOnly,
|
||||
parameters: {
|
||||
nodeIndex: this.nodeIndex,
|
||||
type: inputName,
|
||||
index,
|
||||
},
|
||||
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
|
||||
dragAllowedWhenFull: false,
|
||||
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
|
||||
};
|
||||
|
||||
if (nodeTypeData.outputNames) {
|
||||
// Apply output names if they got set
|
||||
newEndpointData.overlays = [
|
||||
['Label',
|
||||
{
|
||||
id: 'output-name-label',
|
||||
location: [1.75, 0.5],
|
||||
label: nodeTypeData.outputNames[index],
|
||||
cssClass: 'node-output-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
CanvasHelpers.getOutputNameOverlay(nodeTypeData.outputNames[index]),
|
||||
];
|
||||
}
|
||||
|
||||
this.instance.addEndpoint(this.nodeName, newEndpointData);
|
||||
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
|
||||
endpoint.__meta = {
|
||||
nodeName: node.name,
|
||||
index: i,
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
__makeInstanceDraggable(node: INodeUi) {
|
||||
// TODO: This caused problems with displaying old information
|
||||
// https://github.com/jsplumb/katavorio/wiki
|
||||
// https://jsplumb.github.io/jsplumb/home.html
|
||||
// Make nodes draggable
|
||||
this.instance.draggable(this.nodeName, {
|
||||
grid: [10, 10],
|
||||
this.instance.draggable(this.nodeId, {
|
||||
grid: [CanvasHelpers.GRID_SIZE, CanvasHelpers.GRID_SIZE],
|
||||
start: (params: { e: MouseEvent }) => {
|
||||
if (this.isReadOnly === true) {
|
||||
// Do not allow to move nodes in readOnly mode
|
||||
|
@ -305,7 +196,7 @@ export const nodeBase = mixins(
|
|||
// even though "start" and "drag" gets called for all. So lets do for now
|
||||
// some dirty DOM query to get the new positions till I have more time to
|
||||
// create a proper solution
|
||||
let newNodePositon: XYPositon;
|
||||
let newNodePositon: XYPosition;
|
||||
moveNodes.forEach((node: INodeUi) => {
|
||||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||
const element = document.getElementById(nodeElement);
|
||||
|
@ -328,11 +219,23 @@ export const nodeBase = mixins(
|
|||
|
||||
this.$store.commit('updateNodeProperties', updateInformation);
|
||||
});
|
||||
|
||||
this.$emit('moved', node);
|
||||
}
|
||||
},
|
||||
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
||||
});
|
||||
},
|
||||
__addNode (node: INodeUi) {
|
||||
let nodeTypeData = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
|
||||
if (!nodeTypeData) {
|
||||
// If node type is not know use by default the base.noOp data to display it
|
||||
nodeTypeData = this.$store.getters.nodeType(NO_OP_NODE_TYPE) as INodeTypeDescription;
|
||||
}
|
||||
|
||||
this.__addInputEndpoints(node, nodeTypeData);
|
||||
this.__addOutputEndpoints(node, nodeTypeData);
|
||||
this.__makeInstanceDraggable(node);
|
||||
},
|
||||
touchEnd(e: MouseEvent) {
|
||||
if (this.isTouchDevice) {
|
||||
|
|
|
@ -344,6 +344,7 @@ export const nodeHelpers = mixins(
|
|||
};
|
||||
|
||||
this.$store.commit('updateNodeProperties', updateInformation);
|
||||
this.$store.commit('clearNodeExecutionData', node.name);
|
||||
this.updateNodeParameterIssues(node);
|
||||
this.updateNodeCredentialIssues(node);
|
||||
}
|
||||
|
|
|
@ -110,7 +110,8 @@ export const showMessage = mixins(externalHooks).extend({
|
|||
return errorMessage;
|
||||
},
|
||||
|
||||
$showError(error: Error, title: string, message?: string) {
|
||||
$showError(e: Error | unknown, title: string, message?: string) {
|
||||
const error = e as Error;
|
||||
const messageLine = message ? `${message}<br/>` : '';
|
||||
this.$showMessage({
|
||||
title,
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
IWorkflowData,
|
||||
IWorkflowDb,
|
||||
IWorkflowDataUpdate,
|
||||
XYPositon,
|
||||
XYPosition,
|
||||
ITag,
|
||||
IUpdateInformation,
|
||||
} from '../../Interface';
|
||||
|
@ -225,7 +225,7 @@ export const workflowHelpers = mixins(
|
|||
return [];
|
||||
},
|
||||
getByName: (nodeType: string): INodeType | INodeVersionedType | undefined => {
|
||||
const nodeTypeDescription = this.$store.getters.nodeType(nodeType);
|
||||
const nodeTypeDescription = this.$store.getters.nodeType(nodeType) as INodeTypeDescription | null;
|
||||
|
||||
if (nodeTypeDescription === null) {
|
||||
return undefined;
|
||||
|
@ -236,7 +236,7 @@ export const workflowHelpers = mixins(
|
|||
};
|
||||
},
|
||||
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
|
||||
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version);
|
||||
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version) as INodeTypeDescription | null;
|
||||
|
||||
if (nodeTypeDescription === null) {
|
||||
return undefined;
|
||||
|
@ -329,7 +329,7 @@ export const workflowHelpers = mixins(
|
|||
|
||||
// Get the data of the node type that we can get the default values
|
||||
// TODO: Later also has to care about the node-type-version as defaults could be different
|
||||
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription;
|
||||
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription | null;
|
||||
|
||||
if (nodeType !== null) {
|
||||
// Node-Type is known so we can save the parameters correctly
|
||||
|
@ -362,11 +362,6 @@ export const workflowHelpers = mixins(
|
|||
nodeData.credentials = saveCredenetials;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the node color only if it is different to the default color
|
||||
if (node.color && node.color !== nodeType.defaults.color) {
|
||||
nodeData.color = node.color;
|
||||
}
|
||||
} else {
|
||||
// Node-Type is not known so save the data as it is
|
||||
nodeData.credentials = node.credentials;
|
||||
|
@ -568,7 +563,7 @@ export const workflowHelpers = mixins(
|
|||
|
||||
// Updates the position of all the nodes that the top-left node
|
||||
// is at the given position
|
||||
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPositon): void {
|
||||
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPosition): void {
|
||||
if (workflowData.nodes === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -191,6 +191,7 @@ export const workflowRun = mixins(
|
|||
},
|
||||
};
|
||||
this.$store.commit('setWorkflowExecutionData', executionData);
|
||||
this.updateNodesExecutionIssues();
|
||||
|
||||
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
||||
|
||||
|
|
|
@ -116,3 +116,4 @@ export const COMPANY_SIZE_PERSONAL_USE = 'personalUser';
|
|||
|
||||
export const CODING_SKILL_KEY = 'codingSkill';
|
||||
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ const module: Module<ICredentialsState, IRootState> = {
|
|||
},
|
||||
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
|
||||
return (credentialType: string): ICredentialsResponse[] => {
|
||||
return getters.allCredentialsByType[credentialType];
|
||||
return getters.allCredentialsByType[credentialType] || [];
|
||||
};
|
||||
},
|
||||
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
|
||||
|
|
|
@ -28,8 +28,6 @@ $--badge-warning-color: #6b5900;
|
|||
// Warning tooltip
|
||||
$--warning-tooltip-color: #ff8080;
|
||||
|
||||
$--custom-node-view-background : #faf9fe;
|
||||
|
||||
// Table
|
||||
$--custom-table-background-main: $--custom-header-background;
|
||||
$--custom-table-background-stripe-color: #f6f6f6;
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
@import "~n8n-design-system/theme/dist/index.css";
|
||||
|
||||
|
||||
body {
|
||||
background-color: $--custom-node-view-background;
|
||||
background-color: var(--color-canvas-background);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
|
|
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
|
@ -0,0 +1,779 @@
|
|||
/**
|
||||
* Custom connector type
|
||||
* Based on jsplumb Flowchart and Bezier types
|
||||
*
|
||||
* Source GitHub repository:
|
||||
* https://github.com/jsplumb/jsplumb
|
||||
*
|
||||
* Source files:
|
||||
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-flowchart.js
|
||||
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-bezier.js
|
||||
*
|
||||
*
|
||||
* All 1.x.x and 2.x.x versions of jsPlumb Community edition, and so also the
|
||||
* content of this file, are dual-licensed under both MIT and GPLv2.
|
||||
*
|
||||
* MIT LICENSE
|
||||
*
|
||||
* Copyright (c) 2010 - 2014 jsPlumb, http://jsplumbtoolkit.com/
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* ===============================================================================
|
||||
* GNU GENERAL PUBLIC LICENSE
|
||||
* Version 2, June 1991
|
||||
*
|
||||
* Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
* Everyone is permitted to copy and distribute verbatim copies
|
||||
* of this license document, but changing it is not allowed.
|
||||
*
|
||||
* Preamble
|
||||
*
|
||||
* The licenses for most software are designed to take away your
|
||||
* freedom to share and change it. By contrast, the GNU General Public
|
||||
* License is intended to guarantee your freedom to share and change free
|
||||
* software--to make sure the software is free for all its users. This
|
||||
* General Public License applies to most of the Free Software
|
||||
* Foundation's software and to any other program whose authors commit to
|
||||
* using it. (Some other Free Software Foundation software is covered by
|
||||
* the GNU Lesser General Public License instead.) You can apply it to
|
||||
* your programs, too.
|
||||
*
|
||||
* When we speak of free software, we are referring to freedom, not
|
||||
* price. Our General Public Licenses are designed to make sure that you
|
||||
* have the freedom to distribute copies of free software (and charge for
|
||||
* this service if you wish), that you receive source code or can get it
|
||||
* if you want it, that you can change the software or use pieces of it
|
||||
* in new free programs; and that you know you can do these things.
|
||||
*
|
||||
* To protect your rights, we need to make restrictions that forbid
|
||||
* anyone to deny you these rights or to ask you to surrender the rights.
|
||||
* These restrictions translate to certain responsibilities for you if you
|
||||
* distribute copies of the software, or if you modify it.
|
||||
*
|
||||
* For example, if you distribute copies of such a program, whether
|
||||
* gratis or for a fee, you must give the recipients all the rights that
|
||||
* you have. You must make sure that they, too, receive or can get the
|
||||
* source code. And you must show them these terms so they know their
|
||||
* rights.
|
||||
*
|
||||
* We protect your rights with two steps: (1) copyright the software, and
|
||||
* (2) offer you this license which gives you legal permission to copy,
|
||||
* distribute and/or modify the software.
|
||||
*
|
||||
* Also, for each author's protection and ours, we want to make certain
|
||||
* that everyone understands that there is no warranty for this free
|
||||
* software. If the software is modified by someone else and passed on, we
|
||||
* want its recipients to know that what they have is not the original, so
|
||||
* that any problems introduced by others will not reflect on the original
|
||||
* authors' reputations.
|
||||
*
|
||||
* Finally, any free program is threatened constantly by software
|
||||
* patents. We wish to avoid the danger that redistributors of a free
|
||||
* program will individually obtain patent licenses, in effect making the
|
||||
* program proprietary. To prevent this, we have made it clear that any
|
||||
* patent must be licensed for everyone's free use or not licensed at all.
|
||||
*
|
||||
* The precise terms and conditions for copying, distribution and
|
||||
* modification follow.
|
||||
*
|
||||
* GNU GENERAL PUBLIC LICENSE
|
||||
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
*
|
||||
* 0. This License applies to any program or other work which contains
|
||||
* a notice placed by the copyright holder saying it may be distributed
|
||||
* under the terms of this General Public License. The "Program", below,
|
||||
* refers to any such program or work, and a "work based on the Program"
|
||||
* means either the Program or any derivative work under copyright law:
|
||||
* that is to say, a work containing the Program or a portion of it,
|
||||
* either verbatim or with modifications and/or translated into another
|
||||
* language. (Hereinafter, translation is included without limitation in
|
||||
* the term "modification".) Each licensee is addressed as "you".
|
||||
*
|
||||
* Activities other than copying, distribution and modification are not
|
||||
* covered by this License; they are outside its scope. The act of
|
||||
* running the Program is not restricted, and the output from the Program
|
||||
* is covered only if its contents constitute a work based on the
|
||||
* Program (independent of having been made by running the Program).
|
||||
* Whether that is true depends on what the Program does.
|
||||
*
|
||||
* 1. You may copy and distribute verbatim copies of the Program's
|
||||
* source code as you receive it, in any medium, provided that you
|
||||
* conspicuously and appropriately publish on each copy an appropriate
|
||||
* copyright notice and disclaimer of warranty; keep intact all the
|
||||
* notices that refer to this License and to the absence of any warranty;
|
||||
* and give any other recipients of the Program a copy of this License
|
||||
* along with the Program.
|
||||
*
|
||||
* You may charge a fee for the physical act of transferring a copy, and
|
||||
* you may at your option offer warranty protection in exchange for a fee.
|
||||
*
|
||||
* 2. You may modify your copy or copies of the Program or any portion
|
||||
* of it, thus forming a work based on the Program, and copy and
|
||||
* distribute such modifications or work under the terms of Section 1
|
||||
* above, provided that you also meet all of these conditions:
|
||||
*
|
||||
* a) You must cause the modified files to carry prominent notices
|
||||
* stating that you changed the files and the date of any change.
|
||||
*
|
||||
* b) You must cause any work that you distribute or publish, that in
|
||||
* whole or in part contains or is derived from the Program or any
|
||||
* part thereof, to be licensed as a whole at no charge to all third
|
||||
* parties under the terms of this License.
|
||||
*
|
||||
* c) If the modified program normally reads commands interactively
|
||||
* when run, you must cause it, when started running for such
|
||||
* interactive use in the most ordinary way, to print or display an
|
||||
* announcement including an appropriate copyright notice and a
|
||||
* notice that there is no warranty (or else, saying that you provide
|
||||
* a warranty) and that users may redistribute the program under
|
||||
* these conditions, and telling the user how to view a copy of this
|
||||
* License. (Exception: if the Program itself is interactive but
|
||||
* does not normally print such an announcement, your work based on
|
||||
* the Program is not required to print an announcement.)
|
||||
*
|
||||
* These requirements apply to the modified work as a whole. If
|
||||
* identifiable sections of that work are not derived from the Program,
|
||||
* and can be reasonably considered independent and separate works in
|
||||
* themselves, then this License, and its terms, do not apply to those
|
||||
* sections when you distribute them as separate works. But when you
|
||||
* distribute the same sections as part of a whole which is a work based
|
||||
* on the Program, the distribution of the whole must be on the terms of
|
||||
* this License, whose permissions for other licensees extend to the
|
||||
* entire whole, and thus to each and every part regardless of who wrote it.
|
||||
*
|
||||
* Thus, it is not the intent of this section to claim rights or contest
|
||||
* your rights to work written entirely by you; rather, the intent is to
|
||||
* exercise the right to control the distribution of derivative or
|
||||
* collective works based on the Program.
|
||||
*
|
||||
* In addition, mere aggregation of another work not based on the Program
|
||||
* with the Program (or with a work based on the Program) on a volume of
|
||||
* a storage or distribution medium does not bring the other work under
|
||||
* the scope of this License.
|
||||
*
|
||||
* 3. You may copy and distribute the Program (or a work based on it,
|
||||
* under Section 2) in object code or executable form under the terms of
|
||||
* Sections 1 and 2 above provided that you also do one of the following:
|
||||
*
|
||||
* a) Accompany it with the complete corresponding machine-readable
|
||||
* source code, which must be distributed under the terms of Sections
|
||||
* 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
*
|
||||
* b) Accompany it with a written offer, valid for at least three
|
||||
* years, to give any third party, for a charge no more than your
|
||||
* cost of physically performing source distribution, a complete
|
||||
* machine-readable copy of the corresponding source code, to be
|
||||
* distributed under the terms of Sections 1 and 2 above on a medium
|
||||
* customarily used for software interchange; or,
|
||||
*
|
||||
* c) Accompany it with the information you received as to the offer
|
||||
* to distribute corresponding source code. (This alternative is
|
||||
* allowed only for noncommercial distribution and only if you
|
||||
* received the program in object code or executable form with such
|
||||
* an offer, in accord with Subsection b above.)
|
||||
*
|
||||
* The source code for a work means the preferred form of the work for
|
||||
* making modifications to it. For an executable work, complete source
|
||||
* code means all the source code for all modules it contains, plus any
|
||||
* associated interface definition files, plus the scripts used to
|
||||
* control compilation and installation of the executable. However, as a
|
||||
* special exception, the source code distributed need not include
|
||||
* anything that is normally distributed (in either source or binary
|
||||
* form) with the major components (compiler, kernel, and so on) of the
|
||||
* operating system on which the executable runs, unless that component
|
||||
* itself accompanies the executable.
|
||||
*
|
||||
* If distribution of executable or object code is made by offering
|
||||
* access to copy from a designated place, then offering equivalent
|
||||
* access to copy the source code from the same place counts as
|
||||
* distribution of the source code, even though third parties are not
|
||||
* compelled to copy the source along with the object code.
|
||||
*
|
||||
* 4. You may not copy, modify, sublicense, or distribute the Program
|
||||
* except as expressly provided under this License. Any attempt
|
||||
* otherwise to copy, modify, sublicense or distribute the Program is
|
||||
* void, and will automatically terminate your rights under this License.
|
||||
* However, parties who have received copies, or rights, from you under
|
||||
* this License will not have their licenses terminated so long as such
|
||||
* parties remain in full compliance.
|
||||
*
|
||||
* 5. You are not required to accept this License, since you have not
|
||||
* signed it. However, nothing else grants you permission to modify or
|
||||
* distribute the Program or its derivative works. These actions are
|
||||
* prohibited by law if you do not accept this License. Therefore, by
|
||||
* modifying or distributing the Program (or any work based on the
|
||||
* Program), you indicate your acceptance of this License to do so, and
|
||||
* all its terms and conditions for copying, distributing or modifying
|
||||
* the Program or works based on it.
|
||||
*
|
||||
* 6. Each time you redistribute the Program (or any work based on the
|
||||
* Program), the recipient automatically receives a license from the
|
||||
* original licensor to copy, distribute or modify the Program subject to
|
||||
* these terms and conditions. You may not impose any further
|
||||
* restrictions on the recipients' exercise of the rights granted herein.
|
||||
* You are not responsible for enforcing compliance by third parties to
|
||||
* this License.
|
||||
*
|
||||
* 7. If, as a consequence of a court judgment or allegation of patent
|
||||
* infringement or for any other reason (not limited to patent issues),
|
||||
* conditions are imposed on you (whether by court order, agreement or
|
||||
* otherwise) that contradict the conditions of this License, they do not
|
||||
* excuse you from the conditions of this License. If you cannot
|
||||
* distribute so as to satisfy simultaneously your obligations under this
|
||||
* License and any other pertinent obligations, then as a consequence you
|
||||
* may not distribute the Program at all. For example, if a patent
|
||||
* license would not permit royalty-free redistribution of the Program by
|
||||
* all those who receive copies directly or indirectly through you, then
|
||||
* the only way you could satisfy both it and this License would be to
|
||||
* refrain entirely from distribution of the Program.
|
||||
*
|
||||
* If any portion of this section is held invalid or unenforceable under
|
||||
* any particular circumstance, the balance of the section is intended to
|
||||
* apply and the section as a whole is intended to apply in other
|
||||
* circumstances.
|
||||
*
|
||||
* It is not the purpose of this section to induce you to infringe any
|
||||
* patents or other property right claims or to contest validity of any
|
||||
* such claims; this section has the sole purpose of protecting the
|
||||
* integrity of the free software distribution system, which is
|
||||
* implemented by public license practices. Many people have made
|
||||
* generous contributions to the wide range of software distributed
|
||||
* through that system in reliance on consistent application of that
|
||||
* system; it is up to the author/donor to decide if he or she is willing
|
||||
* to distribute software through any other system and a licensee cannot
|
||||
* impose that choice.
|
||||
*
|
||||
* This section is intended to make thoroughly clear what is believed to
|
||||
* be a consequence of the rest of this License.
|
||||
*
|
||||
* 8. If the distribution and/or use of the Program is restricted in
|
||||
* certain countries either by patents or by copyrighted interfaces, the
|
||||
* original copyright holder who places the Program under this License
|
||||
* may add an explicit geographical distribution limitation excluding
|
||||
* those countries, so that distribution is permitted only in or among
|
||||
* countries not thus excluded. In such case, this License incorporates
|
||||
* the limitation as if written in the body of this License.
|
||||
*
|
||||
* 9. The Free Software Foundation may publish revised and/or new versions
|
||||
* of the General Public License from time to time. Such new versions will
|
||||
* be similar in spirit to the present version, but may differ in detail to
|
||||
* address new problems or concerns.
|
||||
*
|
||||
* Each version is given a distinguishing version number. If the Program
|
||||
* specifies a version number of this License which applies to it and "any
|
||||
* later version", you have the option of following the terms and conditions
|
||||
* either of that version or of any later version published by the Free
|
||||
* Software Foundation. If the Program does not specify a version number of
|
||||
* this License, you may choose any version ever published by the Free Software
|
||||
* Foundation.
|
||||
*
|
||||
* 10. If you wish to incorporate parts of the Program into other free
|
||||
* programs whose distribution conditions are different, write to the author
|
||||
* to ask for permission. For software which is copyrighted by the Free
|
||||
* Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
* make exceptions for this. Our decision will be guided by the two goals
|
||||
* of preserving the free status of all derivatives of our free software and
|
||||
* of promoting the sharing and reuse of software generally.
|
||||
*
|
||||
* NO WARRANTY
|
||||
*
|
||||
* 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
* FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
* OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
* PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
* OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
* TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
* PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
* REPAIR OR CORRECTION.
|
||||
*
|
||||
* 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
* WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
* REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
* INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
* OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
* TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
* YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
* PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGES.
|
||||
*
|
||||
*/
|
||||
(function () {
|
||||
|
||||
"use strict";
|
||||
var root = this, _jp = root.jsPlumb, _ju = root.jsPlumbUtil, _jg = root.Biltong;
|
||||
var STRAIGHT = "Straight";
|
||||
var ARC = "Arc";
|
||||
|
||||
/**
|
||||
* Custom connector type
|
||||
*
|
||||
* @param stub {number} length of stub segments in flowchart
|
||||
* @param getEndpointOffset {Function} callback to offset stub length based on endpoint in flowchart
|
||||
* @param midpoint {number} float percent of halfway point of segments in flowchart
|
||||
* @param loopbackVerticalLength {number} height of vertical segment when looping in flowchart
|
||||
* @param cornerRadius {number} radius of flowchart connectors
|
||||
* @param loopbackMinimum {number} minimum threshold before looping behavior takes effect in flowchart
|
||||
* @param targetGap {number} gap between connector and target endpoint in both flowchart and bezier
|
||||
*/
|
||||
const N8nCustom = function (params) {
|
||||
params = params || {};
|
||||
this.type = "N8nCustom";
|
||||
|
||||
params.stub = params.stub == null ? 30 : params.stub;
|
||||
|
||||
var _super = _jp.Connectors.AbstractConnector.apply(this, arguments),
|
||||
minorAnchor = 0, // seems to be angle at which connector leaves endpoint
|
||||
majorAnchor = 0, // translates to curviness of bezier curve
|
||||
segments,
|
||||
midpoint = params.midpoint == null ? 0.5 : params.midpoint,
|
||||
alwaysRespectStubs = params.alwaysRespectStubs === true,
|
||||
loopbackVerticalLength = params.loopbackVerticalLength || 0,
|
||||
lastx = null, lasty = null,
|
||||
cornerRadius = params.cornerRadius != null ? params.cornerRadius : 0,
|
||||
loopbackMinimum = params.loopbackMinimum || 100,
|
||||
curvinessCoeffient = 0.4,
|
||||
zBezierOffset = 40,
|
||||
targetGap = params.targetGap || 0,
|
||||
stub = params.stub || 0;
|
||||
|
||||
/**
|
||||
* Set target endpoint
|
||||
* (to override default behavior tracking mouse when dragging mouse)
|
||||
* @param {Endpoint} endpoint
|
||||
*/
|
||||
this.setTargetEndpoint = function (endpoint) {
|
||||
this.overrideTargetEndpoint = endpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* reset target endpoint overriding default behavior
|
||||
*/
|
||||
this.resetTargetEndpoint = function () {
|
||||
this.overrideTargetEndpoint = null;
|
||||
};
|
||||
|
||||
this._compute = function (originalPaintInfo, connParams) {
|
||||
const paintInfo = _getPaintInfo(connParams, { targetGap, stub, overrideTargetEndpoint: this.overrideTargetEndpoint, getEndpointOffset: params.getEndpointOffset });
|
||||
Object.keys(paintInfo).forEach((key) => {
|
||||
// override so that bounding box is calculated correctly wheen target override is set
|
||||
originalPaintInfo[key] = paintInfo[key];
|
||||
});
|
||||
|
||||
if (paintInfo.tx < 0) {
|
||||
this._computeFlowchart(paintInfo);
|
||||
}
|
||||
else {
|
||||
this._computeBezier(paintInfo);
|
||||
}
|
||||
};
|
||||
|
||||
this._computeBezier = function (paintInfo) {
|
||||
var sp = paintInfo.sourcePos,
|
||||
tp = paintInfo.targetPos,
|
||||
_w = Math.abs(sp[0] - tp[0]) - paintInfo.targetGap,
|
||||
_h = Math.abs(sp[1] - tp[1]);
|
||||
|
||||
var _CP, _CP2,
|
||||
_sx = sp[0] < tp[0] ? _w : 0,
|
||||
_sy = sp[1] < tp[1] ? _h : 0,
|
||||
_tx = sp[0] < tp[0] ? 0 : _w,
|
||||
_ty = sp[1] < tp[1] ? 0 : _h;
|
||||
|
||||
if (paintInfo.ySpan <= 20 || (paintInfo.ySpan <= 100 && paintInfo.xSpan <= 100)) {
|
||||
majorAnchor = 0.1;
|
||||
}
|
||||
else {
|
||||
majorAnchor = paintInfo.xSpan * curvinessCoeffient + zBezierOffset;
|
||||
}
|
||||
|
||||
_CP = _findControlPoint([_sx, _sy], sp, tp, paintInfo.sourceEndpoint, paintInfo.targetEndpoint, paintInfo.so, paintInfo.to, majorAnchor, minorAnchor);
|
||||
_CP2 = _findControlPoint([_tx, _ty], tp, sp, paintInfo.targetEndpoint, paintInfo.sourceEndpoint, paintInfo.to, paintInfo.so, majorAnchor, minorAnchor);
|
||||
|
||||
_super.addSegment(this, "Bezier", {
|
||||
x1: _sx, y1: _sy, x2: _tx, y2: _ty,
|
||||
cp1x: _CP[0], cp1y: _CP[1], cp2x: _CP2[0], cp2y: _CP2[1],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* helper method to add a segment.
|
||||
*/
|
||||
const addFlowchartSegment = function (segments, x, y, paintInfo) {
|
||||
if (lastx === x && lasty === y) {
|
||||
return;
|
||||
}
|
||||
var lx = lastx == null ? paintInfo.sx : lastx,
|
||||
ly = lasty == null ? paintInfo.sy : lasty,
|
||||
o = lx === x ? "v" : "h";
|
||||
|
||||
lastx = x;
|
||||
lasty = y;
|
||||
segments.push([ lx, ly, x, y, o ]);
|
||||
};
|
||||
|
||||
this._computeFlowchart = function (paintInfo) {
|
||||
|
||||
segments = [];
|
||||
lastx = null;
|
||||
lasty = null;
|
||||
|
||||
// calculate Stubs.
|
||||
var stubs = calcualteStubSegment(paintInfo, {alwaysRespectStubs});
|
||||
|
||||
// add the start stub segment. use stubs for loopback as it will look better, with the loop spaced
|
||||
// away from the element.
|
||||
addFlowchartSegment(segments, stubs[0], stubs[1], paintInfo);
|
||||
|
||||
// compute the rest of the line
|
||||
var p = calculateLineSegment(paintInfo, stubs, {midpoint, loopbackMinimum, loopbackVerticalLength});
|
||||
if (p) {
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
addFlowchartSegment(segments, p[i][0], p[i][1], paintInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// line to end stub
|
||||
addFlowchartSegment(segments, stubs[2], stubs[3], paintInfo);
|
||||
|
||||
// end stub to end (common)
|
||||
addFlowchartSegment(segments, paintInfo.tx, paintInfo.ty, paintInfo);
|
||||
|
||||
// write out the segments.
|
||||
writeFlowchartSegments(_super, this, segments, paintInfo, cornerRadius);
|
||||
};
|
||||
};
|
||||
|
||||
_jp.Connectors.N8nCustom = N8nCustom;
|
||||
_ju.extend(_jp.Connectors.N8nCustom, _jp.Connectors.AbstractConnector);
|
||||
|
||||
|
||||
function _findControlPoint(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint, soo, too, majorAnchor, minorAnchor) {
|
||||
// determine if the two anchors are perpendicular to each other in their orientation. we swap the control
|
||||
// points around if so (code could be tightened up)
|
||||
var perpendicular = soo[0] !== too[0] || soo[1] === too[1],
|
||||
p = [];
|
||||
|
||||
if (!perpendicular) {
|
||||
if (soo[0] === 0) {
|
||||
p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[0] - (majorAnchor * soo[0]));
|
||||
}
|
||||
|
||||
if (soo[1] === 0) {
|
||||
p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[1] + (majorAnchor * too[1]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (too[0] === 0) {
|
||||
p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[0] + (majorAnchor * too[0]));
|
||||
}
|
||||
|
||||
if (too[1] === 0) {
|
||||
p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[1] + (majorAnchor * soo[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
|
||||
function sgn(n) {
|
||||
return n < 0 ? -1 : n === 0 ? 0 : 1;
|
||||
};
|
||||
|
||||
function getFlowchartSegmentDirections(segment) {
|
||||
return [
|
||||
sgn( segment[2] - segment[0] ),
|
||||
sgn( segment[3] - segment[1] ),
|
||||
];
|
||||
};
|
||||
|
||||
function getSegmentLength(s) {
|
||||
return Math.sqrt(Math.pow(s[0] - s[2], 2) + Math.pow(s[1] - s[3], 2));
|
||||
};
|
||||
|
||||
function _cloneArray(a) {
|
||||
var _a = [];
|
||||
_a.push.apply(_a, a);
|
||||
return _a;
|
||||
};
|
||||
|
||||
function writeFlowchartSegments(_super, conn, segments, paintInfo, cornerRadius) {
|
||||
var current = null, next, currentDirection, nextDirection;
|
||||
for (var i = 0; i < segments.length - 1; i++) {
|
||||
|
||||
current = current || _cloneArray(segments[i]);
|
||||
next = _cloneArray(segments[i + 1]);
|
||||
|
||||
currentDirection = getFlowchartSegmentDirections(current);
|
||||
nextDirection = getFlowchartSegmentDirections(next);
|
||||
|
||||
if (cornerRadius > 0 && current[4] !== next[4]) {
|
||||
|
||||
var minSegLength = Math.min(getSegmentLength(current), getSegmentLength(next));
|
||||
var radiusToUse = Math.min(cornerRadius, minSegLength / 2);
|
||||
|
||||
current[2] -= currentDirection[0] * radiusToUse;
|
||||
current[3] -= currentDirection[1] * radiusToUse;
|
||||
next[0] += nextDirection[0] * radiusToUse;
|
||||
next[1] += nextDirection[1] * radiusToUse;
|
||||
|
||||
var ac = (currentDirection[1] === nextDirection[0] && nextDirection[0] === 1) ||
|
||||
((currentDirection[1] === nextDirection[0] && nextDirection[0] === 0) && currentDirection[0] !== nextDirection[1]) ||
|
||||
(currentDirection[1] === nextDirection[0] && nextDirection[0] === -1),
|
||||
sgny = next[1] > current[3] ? 1 : -1,
|
||||
sgnx = next[0] > current[2] ? 1 : -1,
|
||||
sgnEqual = sgny === sgnx,
|
||||
cx = (sgnEqual && ac || (!sgnEqual && !ac)) ? next[0] : current[2],
|
||||
cy = (sgnEqual && ac || (!sgnEqual && !ac)) ? current[3] : next[1];
|
||||
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: current[0], y1: current[1], x2: current[2], y2: current[3],
|
||||
});
|
||||
|
||||
_super.addSegment(conn, ARC, {
|
||||
r: radiusToUse,
|
||||
x1: current[2],
|
||||
y1: current[3],
|
||||
x2: next[0],
|
||||
y2: next[1],
|
||||
cx: cx,
|
||||
cy: cy,
|
||||
ac: ac,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// dx + dy are used to adjust for line width.
|
||||
var dx = (current[2] === current[0]) ? 0 : (current[2] > current[0]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2),
|
||||
dy = (current[3] === current[1]) ? 0 : (current[3] > current[1]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2);
|
||||
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: current[0] - dx, y1: current[1] - dy, x2: current[2] + dx, y2: current[3] + dy,
|
||||
});
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
if (next != null) {
|
||||
// last segment
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: next[0], y1: next[1], x2: next[2], y2: next[3],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const lineCalculators = {
|
||||
opposite: function (paintInfo, {axis, startStub, endStub, idx, midx, midy}) {
|
||||
var pi = paintInfo,
|
||||
comparator = pi["is" + axis.toUpperCase() + "GreaterThanStubTimes2"];
|
||||
|
||||
if (!comparator || (pi.so[idx] === 1 && startStub > endStub) || (pi.so[idx] === -1 && startStub < endStub)) {
|
||||
return {
|
||||
"x": [
|
||||
[startStub, midy],
|
||||
[endStub, midy],
|
||||
],
|
||||
"y": [
|
||||
[midx, startStub],
|
||||
[midx, endStub],
|
||||
],
|
||||
}[axis];
|
||||
}
|
||||
else if ((pi.so[idx] === 1 && startStub < endStub) || (pi.so[idx] === -1 && startStub > endStub)) {
|
||||
return {
|
||||
"x": [
|
||||
[midx, pi.sy],
|
||||
[midx, pi.ty],
|
||||
],
|
||||
"y": [
|
||||
[pi.sx, midy],
|
||||
[pi.tx, midy],
|
||||
],
|
||||
}[axis];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const stubCalculators = {
|
||||
opposite: function (paintInfo, {axis, alwaysRespectStubs}) {
|
||||
var pi = paintInfo,
|
||||
idx = axis === "x" ? 0 : 1,
|
||||
areInProximity = {
|
||||
"x": function () {
|
||||
return ( (pi.so[idx] === 1 && (
|
||||
( (pi.startStubX > pi.endStubX) && (pi.tx > pi.startStubX) ) ||
|
||||
( (pi.sx > pi.endStubX) && (pi.tx > pi.sx))))) ||
|
||||
|
||||
( (pi.so[idx] === -1 && (
|
||||
( (pi.startStubX < pi.endStubX) && (pi.tx < pi.startStubX) ) ||
|
||||
( (pi.sx < pi.endStubX) && (pi.tx < pi.sx)))));
|
||||
},
|
||||
"y": function () {
|
||||
return ( (pi.so[idx] === 1 && (
|
||||
( (pi.startStubY > pi.endStubY) && (pi.ty > pi.startStubY) ) ||
|
||||
( (pi.sy > pi.endStubY) && (pi.ty > pi.sy))))) ||
|
||||
|
||||
( (pi.so[idx] === -1 && (
|
||||
( (pi.startStubY < pi.endStubY) && (pi.ty < pi.startStubY) ) ||
|
||||
( (pi.sy < pi.endStubY) && (pi.ty < pi.sy)))));
|
||||
},
|
||||
};
|
||||
|
||||
if (!alwaysRespectStubs && areInProximity[axis]()) {
|
||||
return {
|
||||
"x": [(paintInfo.sx + paintInfo.tx) / 2, paintInfo.startStubY, (paintInfo.sx + paintInfo.tx) / 2, paintInfo.endStubY],
|
||||
"y": [paintInfo.startStubX, (paintInfo.sy + paintInfo.ty) / 2, paintInfo.endStubX, (paintInfo.sy + paintInfo.ty) / 2],
|
||||
}[axis];
|
||||
}
|
||||
else {
|
||||
return [paintInfo.startStubX, paintInfo.startStubY, paintInfo.endStubX, paintInfo.endStubY];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function calcualteStubSegment(paintInfo, {alwaysRespectStubs}) {
|
||||
return stubCalculators['opposite'](paintInfo, {axis: paintInfo.sourceAxis, alwaysRespectStubs});
|
||||
}
|
||||
|
||||
function calculateLineSegment(paintInfo, stubs, { midpoint, loopbackVerticalLength, loopbackMinimum }) {
|
||||
const axis = paintInfo.sourceAxis,
|
||||
idx = paintInfo.sourceAxis === "x" ? 0 : 1,
|
||||
oidx = paintInfo.sourceAxis === "x" ? 1 : 0,
|
||||
startStub = stubs[idx],
|
||||
otherStartStub = stubs[oidx],
|
||||
endStub = stubs[idx + 2],
|
||||
otherEndStub = stubs[oidx + 2];
|
||||
|
||||
const diffX = paintInfo.endStubX - paintInfo.startStubX;
|
||||
const diffY = paintInfo.endStubY - paintInfo.startStubY;
|
||||
const direction = diffY >= 0 ? 1 : -1; // vertical direction of loop, above or below source
|
||||
|
||||
var midx = paintInfo.startStubX + ((paintInfo.endStubX - paintInfo.startStubX) * midpoint),
|
||||
midy;
|
||||
|
||||
if (diffX < (-1 * loopbackMinimum)) {
|
||||
// loop backward behavior
|
||||
midy = paintInfo.startStubY - (diffX < 0 ? direction * loopbackVerticalLength : 0);
|
||||
} else {
|
||||
// original flowchart behavior
|
||||
midy = paintInfo.startStubY + ((paintInfo.endStubY - paintInfo.startStubY) * midpoint);
|
||||
}
|
||||
|
||||
return lineCalculators['opposite'](paintInfo, {axis, startStub, otherStartStub, endStub, otherEndStub, idx, oidx, midx, midy});
|
||||
}
|
||||
|
||||
function _getPaintInfo(params, { targetGap, stub, overrideTargetEndpoint, getEndpointOffset }) {
|
||||
let { targetPos, targetEndpoint } = params;
|
||||
|
||||
if (
|
||||
overrideTargetEndpoint
|
||||
) {
|
||||
targetPos = overrideTargetEndpoint.anchor.getCurrentLocation();
|
||||
targetEndpoint = overrideTargetEndpoint;
|
||||
}
|
||||
|
||||
const sourceGap = 0;
|
||||
|
||||
stub = stub || 0;
|
||||
const sourceStub = _ju.isArray(stub) ? stub[0] : stub;
|
||||
const targetStub = _ju.isArray(stub) ? stub[1] : stub;
|
||||
|
||||
var segment = _jg.quadrant(params.sourcePos, targetPos),
|
||||
swapX = targetPos[0] < params.sourcePos[0],
|
||||
swapY = targetPos[1] < params.sourcePos[1],
|
||||
lw = params.strokeWidth || 1,
|
||||
so = params.sourceEndpoint.anchor.getOrientation(params.sourceEndpoint), // source orientation
|
||||
to = targetEndpoint.anchor.getOrientation(targetEndpoint), // target orientation
|
||||
x = swapX ? targetPos[0] : params.sourcePos[0],
|
||||
y = swapY ? targetPos[1] : params.sourcePos[1],
|
||||
w = Math.abs(targetPos[0] - params.sourcePos[0]),
|
||||
h = Math.abs(targetPos[1] - params.sourcePos[1]);
|
||||
|
||||
// if either anchor does not have an orientation set, we derive one from their relative
|
||||
// positions. we fix the axis to be the one in which the two elements are further apart, and
|
||||
// point each anchor at the other element. this is also used when dragging a new connection.
|
||||
if (so[0] === 0 && so[1] === 0 || to[0] === 0 && to[1] === 0) {
|
||||
var index = w > h ? 0 : 1, oIndex = [1, 0][index];
|
||||
so = [];
|
||||
to = [];
|
||||
so[index] = params.sourcePos[index] > targetPos[index] ? -1 : 1;
|
||||
to[index] = params.sourcePos[index] > targetPos[index] ? 1 : -1;
|
||||
so[oIndex] = 0;
|
||||
to[oIndex] = 0;
|
||||
}
|
||||
|
||||
const sx = swapX ? w + (sourceGap * so[0]) : sourceGap * so[0],
|
||||
sy = swapY ? h + (sourceGap * so[1]) : sourceGap * so[1],
|
||||
tx = swapX ? targetGap * to[0] : w + (targetGap * to[0]),
|
||||
ty = swapY ? targetGap * to[1] : h + (targetGap * to[1]),
|
||||
oProduct = ((so[0] * to[0]) + (so[1] * to[1]));
|
||||
|
||||
const sourceStubWithOffset = sourceStub + (getEndpointOffset && params.sourceEndpoint ? getEndpointOffset(params.sourceEndpoint) : 0);
|
||||
const targetStubWithOffset = targetStub + (getEndpointOffset && targetEndpoint ? getEndpointOffset(targetEndpoint) : 0);
|
||||
|
||||
// same as paintinfo generated by jsplumb AbstractConnector type
|
||||
var result = {
|
||||
sx: sx, sy: sy, tx: tx, ty: ty, lw: lw,
|
||||
xSpan: Math.abs(tx - sx),
|
||||
ySpan: Math.abs(ty - sy),
|
||||
mx: (sx + tx) / 2,
|
||||
my: (sy + ty) / 2,
|
||||
so: so, to: to, x: x, y: y, w: w, h: h,
|
||||
segment: segment,
|
||||
startStubX: sx + (so[0] * sourceStubWithOffset),
|
||||
startStubY: sy + (so[1] * sourceStubWithOffset),
|
||||
endStubX: tx + (to[0] * targetStubWithOffset),
|
||||
endStubY: ty + (to[1] * targetStubWithOffset),
|
||||
isXGreaterThanStubTimes2: Math.abs(sx - tx) > (sourceStubWithOffset + targetStubWithOffset),
|
||||
isYGreaterThanStubTimes2: Math.abs(sy - ty) > (sourceStubWithOffset + targetStubWithOffset),
|
||||
opposite: oProduct === -1,
|
||||
perpendicular: oProduct === 0,
|
||||
orthogonal: oProduct === 1,
|
||||
sourceAxis: so[0] === 0 ? "y" : "x",
|
||||
points: [x, y, w, h, sx, sy, tx, ty ],
|
||||
stubs:[sourceStubWithOffset, targetStubWithOffset],
|
||||
anchorOrientation: "opposite", // always opposite since our endpoints are always opposite (source orientation is left (1) and target orientaiton is right (-1))
|
||||
|
||||
/** custom keys added */
|
||||
sourceEndpoint: params.sourceEndpoint,
|
||||
targetEndpoint: targetEndpoint,
|
||||
sourcePos: params.sourcePos,
|
||||
targetPos: targetEndpoint.anchor.getCurrentLocation(),
|
||||
targetGap,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
}).call(typeof window !== 'undefined' ? window : this);
|
|
@ -28,7 +28,7 @@ import {
|
|||
IPushDataNodeExecuteAfter,
|
||||
IUpdateInformation,
|
||||
IWorkflowDb,
|
||||
XYPositon,
|
||||
XYPosition,
|
||||
IRestApiContext,
|
||||
} from './Interface';
|
||||
|
||||
|
@ -576,6 +576,13 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
|
||||
},
|
||||
clearNodeExecutionData (state, nodeName: string): void {
|
||||
if (state.workflowExecutionData === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.delete(state.workflowExecutionData.data.resultData.runData, nodeName);
|
||||
},
|
||||
|
||||
setWorkflowSettings (state, workflowSettings: IWorkflowSettings) {
|
||||
Vue.set(state.workflow, 'settings', workflowSettings);
|
||||
|
@ -739,7 +746,7 @@ export const store = new Vuex.Store({
|
|||
return state.nodeIndex[index];
|
||||
},
|
||||
|
||||
getNodeViewOffsetPosition: (state): XYPositon => {
|
||||
getNodeViewOffsetPosition: (state): XYPosition => {
|
||||
return state.nodeViewOffsetPosition;
|
||||
},
|
||||
isNodeViewMoveInProgress: (state): boolean => {
|
||||
|
@ -766,9 +773,7 @@ export const store = new Vuex.Store({
|
|||
allConnections: (state): IConnections => {
|
||||
return state.workflow.connections;
|
||||
},
|
||||
// connectionsByNodeName: (state) => (nodeName: string): {[key: string]: Connection[][]} | null => {
|
||||
// connectionsByNodeName: (state) => (nodeName: string): { [key: string]: NodeConnections} | null => {
|
||||
connectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
|
||||
outgoingConnectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
|
||||
if (state.workflow.connections.hasOwnProperty(nodeName)) {
|
||||
return state.workflow.connections[nodeName];
|
||||
}
|
||||
|
@ -777,15 +782,14 @@ export const store = new Vuex.Store({
|
|||
allNodes: (state): INodeUi[] => {
|
||||
return state.workflow.nodes;
|
||||
},
|
||||
nodeByName: (state) => (nodeName: string): INodeUi | null => {
|
||||
const foundNode = state.workflow.nodes.find(node => {
|
||||
return node.name === nodeName;
|
||||
});
|
||||
|
||||
if (foundNode === undefined) {
|
||||
return null;
|
||||
}
|
||||
return foundNode;
|
||||
nodesByName: (state: IRootState): {[name: string]: INodeUi} => {
|
||||
return state.workflow.nodes.reduce((accu: {[name: string]: INodeUi}, node) => {
|
||||
accu[node.name] = node;
|
||||
return accu;
|
||||
}, {});
|
||||
},
|
||||
getNodeByName: (state, getters) => (nodeName: string): INodeUi | null => {
|
||||
return getters.nodesByName[nodeName] || null;
|
||||
},
|
||||
nodesIssuesExist: (state): boolean => {
|
||||
for (const node of state.workflow.nodes) {
|
||||
|
@ -810,10 +814,10 @@ export const store = new Vuex.Store({
|
|||
return foundType;
|
||||
},
|
||||
activeNode: (state, getters): INodeUi | null => {
|
||||
return getters.nodeByName(state.activeNode);
|
||||
return getters.getNodeByName(state.activeNode);
|
||||
},
|
||||
lastSelectedNode: (state, getters): INodeUi | null => {
|
||||
return getters.nodeByName(state.lastSelectedNode);
|
||||
return getters.getNodeByName(state.lastSelectedNode);
|
||||
},
|
||||
lastSelectedNodeOutputIndex: (state, getters): number | null => {
|
||||
return state.lastSelectedNodeOutputIndex;
|
||||
|
|
File diff suppressed because it is too large
Load diff
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
|
@ -0,0 +1,725 @@
|
|||
import { getStyleTokenValue } from "@/components/helpers";
|
||||
import { START_NODE_TYPE } from "@/constants";
|
||||
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||
import {
|
||||
IConnection,
|
||||
ITaskData,
|
||||
INodeExecutionData,
|
||||
NodeInputConnections,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const OVERLAY_DROP_NODE_ID = 'drop-add-node';
|
||||
export const OVERLAY_MIDPOINT_ARROW_ID = 'midpoint-arrow';
|
||||
export const OVERLAY_ENDPOINT_ARROW_ID = 'endpoint-arrow';
|
||||
export const OVERLAY_RUN_ITEMS_ID = 'run-items-label';
|
||||
export const OVERLAY_CONNECTION_ACTIONS_ID = 'connection-actions';
|
||||
export const JSPLUMB_FLOWCHART_STUB = 26;
|
||||
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION = [-3, .5];
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED = [-4.5, .5];
|
||||
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
||||
export const GRID_SIZE = 20;
|
||||
|
||||
const MIN_X_TO_SHOW_OUTPUT_LABEL = 90;
|
||||
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
||||
|
||||
export const NODE_SIZE = 100;
|
||||
export const DEFAULT_START_POSITION_X = 240;
|
||||
export const DEFAULT_START_POSITION_Y = 300;
|
||||
export const HEADER_HEIGHT = 65;
|
||||
export const SIDEBAR_WIDTH = 65;
|
||||
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300;
|
||||
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
|
||||
const LOOPBACK_MINIMUM = 140;
|
||||
export const INPUT_UUID_KEY = '-input';
|
||||
export const OUTPUT_UUID_KEY = '-output';
|
||||
|
||||
export const DEFAULT_START_NODE = {
|
||||
name: 'Start',
|
||||
type: START_NODE_TYPE,
|
||||
typeVersion: 1,
|
||||
position: [
|
||||
DEFAULT_START_POSITION_X,
|
||||
DEFAULT_START_POSITION_Y,
|
||||
] as XYPosition,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
|
||||
cornerRadius: 12,
|
||||
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||
targetGap: 4,
|
||||
alwaysRespectStubs: false,
|
||||
loopbackVerticalLength: NODE_SIZE, // height of vertical segment when looping
|
||||
loopbackMinimum: LOOPBACK_MINIMUM, // minimum length before flowchart loops around
|
||||
getEndpointOffset(endpoint: Endpoint) {
|
||||
const indexOffset = 10; // stub offset between different endpoints of same node
|
||||
const index = endpoint && endpoint.__meta ? endpoint.__meta.index : 0;
|
||||
|
||||
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
||||
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
|
||||
|
||||
return index * indexOffset + labelOffset;
|
||||
},
|
||||
}];
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
||||
stroke: getStyleTokenValue('--color-foreground-dark'),
|
||||
strokeWidth: 2,
|
||||
outlineWidth: 12,
|
||||
outlineStroke: 'transparent',
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_PULL: PaintStyle = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-foreground-xdark'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_PRIMARY = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-primary'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_SUCCESS = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-success-light'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
id: OVERLAY_ENDPOINT_ARROW_ID,
|
||||
location: 1,
|
||||
width: 12,
|
||||
foldback: 1,
|
||||
length: 10,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
id: OVERLAY_MIDPOINT_ARROW_ID,
|
||||
location: 0.5,
|
||||
width: 12,
|
||||
foldback: 1,
|
||||
length: 10,
|
||||
visible: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
export const CONNECTOR_DROP_NODE_OVERLAY: OverlaySpec[] = [
|
||||
[
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_DROP_NODE_ID,
|
||||
label: 'Drop connection<br />to add node',
|
||||
cssClass: 'drop-add-node-label',
|
||||
location: 0.5,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
export const ANCHOR_POSITIONS: {
|
||||
[key: string]: {
|
||||
[key: number]: string[] | number[][];
|
||||
}
|
||||
} = {
|
||||
input: {
|
||||
1: [
|
||||
[0.01, 0.5, -1, 0],
|
||||
],
|
||||
2: [
|
||||
[0.01, 0.3, -1, 0],
|
||||
[0.01, 0.7, -1, 0],
|
||||
],
|
||||
3: [
|
||||
[0.01, 0.25, -1, 0],
|
||||
[0.01, 0.5, -1, 0],
|
||||
[0.01, 0.75, -1, 0],
|
||||
],
|
||||
4: [
|
||||
[0.01, 0.2, -1, 0],
|
||||
[0.01, 0.4, -1, 0],
|
||||
[0.01, 0.6, -1, 0],
|
||||
[0.01, 0.8, -1, 0],
|
||||
],
|
||||
},
|
||||
output: {
|
||||
1: [
|
||||
[.99, 0.5, 1, 0],
|
||||
],
|
||||
2: [
|
||||
[.99, 0.3, 1, 0],
|
||||
[.99, 0.7, 1, 0],
|
||||
],
|
||||
3: [
|
||||
[.99, 0.25, 1, 0],
|
||||
[.99, 0.5, 1, 0],
|
||||
[.99, 0.75, 1, 0],
|
||||
],
|
||||
4: [
|
||||
[.99, 0.2, 1, 0],
|
||||
[.99, 0.4, 1, 0],
|
||||
[.99, 0.6, 1, 0],
|
||||
[.99, 0.8, 1, 0],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||
width: 8,
|
||||
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
|
||||
fill: getStyleTokenValue(color),
|
||||
stroke: getStyleTokenValue(color),
|
||||
lineWidth: 0,
|
||||
});
|
||||
|
||||
export const getInputNameOverlay = (label: string) => ([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_INPUT_NAME_LABEL,
|
||||
location: OVERLAY_INPUT_NAME_LABEL_POSITION,
|
||||
label,
|
||||
cssClass: 'node-input-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
]);
|
||||
|
||||
export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||
fill: getStyleTokenValue(color),
|
||||
outlineStroke: 'none',
|
||||
});
|
||||
|
||||
export const getOutputNameOverlay = (label: string) => ([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||
location: [1.9, 0.5],
|
||||
label,
|
||||
cssClass: 'node-output-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
]);
|
||||
|
||||
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
||||
overlays.forEach((overlay: OverlaySpec) => {
|
||||
connection.addOverlay(overlay);
|
||||
});
|
||||
};
|
||||
|
||||
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||
return nodes.reduce((leftmostTop, node) => {
|
||||
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||
return leftmostTop;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
||||
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
||||
if (node.position[0] < accu.minX) {
|
||||
accu.minX = node.position[0];
|
||||
}
|
||||
if (node.position[1] < accu.minY) {
|
||||
accu.minY = node.position[1];
|
||||
}
|
||||
if (node.position[0] > accu.maxX) {
|
||||
accu.maxX = node.position[0];
|
||||
}
|
||||
if (node.position[1] > accu.maxY) {
|
||||
accu.maxY = node.position[1];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {
|
||||
minX: nodes[0].position[0],
|
||||
minY: nodes[0].position[1],
|
||||
maxX: nodes[0].position[0],
|
||||
maxY: nodes[0].position[1],
|
||||
});
|
||||
};
|
||||
|
||||
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale /= 1.25;
|
||||
xOffset /= 1.25;
|
||||
yOffset /= 1.25;
|
||||
xOffset += window.innerWidth / 10;
|
||||
yOffset += window.innerHeight / 10;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale *= 1.25;
|
||||
xOffset -= window.innerWidth / 10;
|
||||
yOffset -= window.innerHeight / 10;
|
||||
xOffset *= 1.25;
|
||||
yOffset *= 1.25;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||
if (config.scale > 1) { // zoomed in
|
||||
while (config.scale > 1) {
|
||||
config = scaleSmaller(config);
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (config.scale < 1) {
|
||||
config = scaleBigger(config);
|
||||
}
|
||||
}
|
||||
|
||||
config.scale = 1;
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
try {
|
||||
return item.getOverlay(overlayId); // handle when _jsPlumb element is deleted
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const showOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
const overlay = getOverlay(item, overlayId);
|
||||
if (overlay) {
|
||||
overlay.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
export const hideOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
const overlay = getOverlay(item, overlayId);
|
||||
if (overlay) {
|
||||
overlay.setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
export const showOrHideMidpointArrow = (connection: Connection) => {
|
||||
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
|
||||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue ? targetEndpoint.anchor.lastReturnValue[0] : sourcePosition + 1; // lastReturnValue is null when moving connections from node to another
|
||||
|
||||
const minimum = hasItemsLabel ? 150 : 0;
|
||||
const isBackwards = sourcePosition >= targetPosition;
|
||||
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
||||
|
||||
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||
if (arrow) {
|
||||
arrow.setVisible(isBackwards && isTooLong);
|
||||
arrow.setLocation(hasItemsLabel ? .6: .5);
|
||||
}
|
||||
};
|
||||
|
||||
export const getConnectorLengths = (connection: Connection): [number, number] => {
|
||||
if (!connection.connector) {
|
||||
return [0, 0];
|
||||
}
|
||||
const bounds = connection.connector.bounds;
|
||||
const diffX = Math.abs(bounds.maxX - bounds.minX);
|
||||
const diffY = Math.abs(bounds.maxY - bounds.minY);
|
||||
|
||||
return [diffX, diffY];
|
||||
};
|
||||
|
||||
const isLoopingBackwards = (connection: Connection) => {
|
||||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue[0];
|
||||
|
||||
return targetPosition - sourcePosition < (-1 * LOOPBACK_MINIMUM);
|
||||
};
|
||||
|
||||
export const showOrHideItemsLabel = (connection: Connection) => {
|
||||
if (!connection || !connection.connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
if (actionsOverlay && actionsOverlay.visible) {
|
||||
overlay.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const [diffX, diffY] = getConnectorLengths(connection);
|
||||
|
||||
if (diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL) {
|
||||
overlay.setVisible(false);
|
||||
}
|
||||
else {
|
||||
overlay.setVisible(true);
|
||||
}
|
||||
|
||||
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
||||
if (innerElement) {
|
||||
if (diffY === 0 || isLoopingBackwards(connection)) {
|
||||
innerElement.classList.add('floating');
|
||||
}
|
||||
else {
|
||||
innerElement.classList.remove('floating');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getIcon = (name: string): string => {
|
||||
if (name === 'trash') {
|
||||
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="trash" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-trash fa-w-14 Icon__medium_ctPPJ"><path data-v-66d5c7e2="" fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" class=""></path></svg>`;
|
||||
}
|
||||
|
||||
if (name === 'plus') {
|
||||
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-plus fa-w-14 Icon__medium_ctPPJ"><path data-v-301ed208="" fill="currentColor" d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z" class=""></path></svg>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
|
||||
if (Math.abs(position1[0] - position2[0]) <= 100) {
|
||||
if (Math.abs(position1[1] - position2[1]) <= 50) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
|
||||
const targetPosition: XYPosition = [...newPosition];
|
||||
|
||||
targetPosition[0] = targetPosition[0] - (targetPosition[0] % GRID_SIZE);
|
||||
targetPosition[1] = targetPosition[1] - (targetPosition[1] % GRID_SIZE);
|
||||
|
||||
if (!movePosition) {
|
||||
movePosition = [40, 40];
|
||||
}
|
||||
|
||||
let conflictFound = false;
|
||||
let i, node;
|
||||
do {
|
||||
conflictFound = false;
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
if (!canUsePosition(node.position, targetPosition)) {
|
||||
conflictFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictFound === true) {
|
||||
targetPosition[0] += movePosition[0];
|
||||
targetPosition[1] += movePosition[1];
|
||||
}
|
||||
} while (conflictFound === true);
|
||||
|
||||
return targetPosition;
|
||||
};
|
||||
|
||||
export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
|
||||
// @ts-ignore
|
||||
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
|
||||
// @ts-ignore
|
||||
const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
export const getRelativePosition = (x: number, y: number, scale: number, offset: XYPosition): XYPosition => {
|
||||
return [
|
||||
(x - offset[0]) / scale,
|
||||
(y - offset[1]) / scale,
|
||||
];
|
||||
};
|
||||
|
||||
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition) => {
|
||||
const squareSize = GRID_SIZE * scale;
|
||||
const dotSize = 1 * scale;
|
||||
const dotPosition = (GRID_SIZE / 2) * scale;
|
||||
const styles: object = {
|
||||
'background-size': `${squareSize}px ${squareSize}px`,
|
||||
'background-position': `left ${offsetPosition[0]}px top ${offsetPosition[1]}px`,
|
||||
};
|
||||
if (squareSize > 10.5) {
|
||||
const dotColor = getStyleTokenValue('--color-canvas-dot');
|
||||
return {
|
||||
...styles,
|
||||
'background-image': `radial-gradient(circle at ${dotPosition}px ${dotPosition}px, ${dotColor} ${dotSize}px, transparent 0)`,
|
||||
};
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
|
||||
export const hideConnectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
}
|
||||
};
|
||||
|
||||
export const showConectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
||||
const outputMap: {[sourceOutputIndex: string]: {[targetNodeName: string]: {[targetInputIndex: string]: {total: number, iterations: number}}}} = {};
|
||||
|
||||
data.forEach((run: ITaskData) => {
|
||||
if (!run.data || !run.data.main) {
|
||||
return;
|
||||
}
|
||||
|
||||
run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => {
|
||||
if (!nodeConnections[i]) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodeConnections[i]
|
||||
.map((connection: IConnection) => {
|
||||
const sourceOutputIndex = i;
|
||||
const targetNodeName = connection.node;
|
||||
const targetInputIndex = connection.index;
|
||||
|
||||
if (!outputMap[sourceOutputIndex]) {
|
||||
outputMap[sourceOutputIndex] = {};
|
||||
}
|
||||
|
||||
if (!outputMap[sourceOutputIndex][targetNodeName]) {
|
||||
outputMap[sourceOutputIndex][targetNodeName] = {};
|
||||
}
|
||||
|
||||
if (!outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]) {
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex] = {
|
||||
total: 0,
|
||||
iterations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].total += output ? output.length : 0;
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].iterations += output ? 1 : 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return outputMap;
|
||||
};
|
||||
|
||||
export const resetConnection = (connection: Connection) => {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
showOrHideMidpointArrow(connection);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.remove('success');
|
||||
}
|
||||
};
|
||||
|
||||
export const addConnectionOutputSuccess = (connection: Connection, output: {total: number, iterations: number}) => {
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.add('success');
|
||||
}
|
||||
|
||||
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
}
|
||||
|
||||
let label = `${output.total}`;
|
||||
label = output.total > 1 ? `${label} items` : `${label} item`;
|
||||
label = output.iterations > 1 ? `${label} total` : label;
|
||||
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_RUN_ITEMS_ID,
|
||||
label: `<span>${label}</span>`,
|
||||
cssClass: 'connection-run-items-label',
|
||||
location: .5,
|
||||
},
|
||||
]);
|
||||
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
};
|
||||
|
||||
|
||||
export const getZoomToFit = (nodes: INodeUi[]): {offset: XYPosition, zoomLevel: number} => {
|
||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||
|
||||
const PADDING = NODE_SIZE * 4;
|
||||
|
||||
const editorWidth = window.innerWidth;
|
||||
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
|
||||
const scaleX = editorWidth / diffX;
|
||||
|
||||
const editorHeight = window.innerHeight;
|
||||
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
|
||||
const scaleY = editorHeight / diffY;
|
||||
|
||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
|
||||
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
|
||||
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
return {
|
||||
zoomLevel,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const getUniqueNodeName = (nodes: INodeUi[], originalName: string, additinalUsedNames?: string[]) => {
|
||||
// Check if node-name is unique else find one that is
|
||||
additinalUsedNames = additinalUsedNames || [];
|
||||
|
||||
// Get all the names of the current nodes
|
||||
const nodeNames = nodes.map((node: INodeUi) => {
|
||||
return node.name;
|
||||
});
|
||||
|
||||
// Check first if the current name is already unique
|
||||
if (!nodeNames.includes(originalName) && !additinalUsedNames.includes(originalName)) {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||
let ignore, baseName, nameIndex, uniqueName;
|
||||
let index = 1;
|
||||
|
||||
if (nameMatch === null) {
|
||||
// Name is only a number
|
||||
index = parseInt(originalName, 10);
|
||||
baseName = '';
|
||||
uniqueName = baseName + index;
|
||||
} else {
|
||||
// Name is string or string/number combination
|
||||
[ignore, baseName, nameIndex] = nameMatch;
|
||||
if (nameIndex !== '') {
|
||||
index = parseInt(nameIndex, 10);
|
||||
}
|
||||
uniqueName = baseName;
|
||||
}
|
||||
|
||||
while (
|
||||
nodeNames.includes(uniqueName) ||
|
||||
additinalUsedNames.includes(uniqueName)
|
||||
) {
|
||||
uniqueName = baseName + (index++);
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||
if (connection && connection.connector) {
|
||||
if (targetEndpoint) {
|
||||
connection.connector.setTargetEndpoint(targetEndpoint);
|
||||
}
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
||||
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
}
|
||||
};
|
||||
|
||||
export const showPullConnectionState = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
||||
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetConnectionAfterPull = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||
if (inputNameOverlay) {
|
||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION);
|
||||
}
|
||||
};
|
||||
|
||||
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||
if (inputNameOverlay) {
|
||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED);
|
||||
}
|
||||
};
|
||||
|
||||
export const addConnectionActionsOverlay = (connection: Connection, onDelete: Function, onAdd: Function) => {
|
||||
if (getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID)) {
|
||||
return; // avoid free floating actions when moving connection from one node to another
|
||||
}
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
label: `<div class="add">${getIcon('plus')}</div> <div class="delete">${getIcon('trash')}</div>`,
|
||||
cssClass: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
visible: false,
|
||||
events: {
|
||||
mousedown: (overlay: Overlay, event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement;
|
||||
if (element.classList.contains('delete') || (element.parentElement && element.parentElement.classList.contains('delete'))) {
|
||||
onDelete();
|
||||
}
|
||||
else if (element.classList.contains('add') || (element.parentElement && element.parentElement.classList.contains('add'))) {
|
||||
onAdd();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export const getOutputEndpointUUID = (nodeIndex: string, outputIndex: number) => {
|
||||
return `${nodeIndex}${OUTPUT_UUID_KEY}${outputIndex}`;
|
||||
};
|
||||
|
||||
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
||||
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
import { INodeUi, IZoomConfig } from "@/Interface";
|
||||
|
||||
interface ICorners {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||
return nodes.reduce((leftmostTop, node) => {
|
||||
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||
return leftmostTop;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkflowCorners = (nodes: INodeUi[]): ICorners => {
|
||||
return nodes.reduce((accu: ICorners, node: INodeUi) => {
|
||||
if (node.position[0] < accu.minX) {
|
||||
accu.minX = node.position[0];
|
||||
}
|
||||
if (node.position[1] < accu.minY) {
|
||||
accu.minY = node.position[1];
|
||||
}
|
||||
if (node.position[0] > accu.maxX) {
|
||||
accu.maxX = node.position[0];
|
||||
}
|
||||
if (node.position[1] > accu.maxY) {
|
||||
accu.maxY = node.position[1];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {
|
||||
minX: nodes[0].position[0],
|
||||
minY: nodes[0].position[1],
|
||||
maxX: nodes[0].position[0],
|
||||
maxY: nodes[0].position[1],
|
||||
});
|
||||
};
|
||||
|
||||
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale /= 1.25;
|
||||
xOffset /= 1.25;
|
||||
yOffset /= 1.25;
|
||||
xOffset += window.innerWidth / 10;
|
||||
yOffset += window.innerHeight / 10;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale *= 1.25;
|
||||
xOffset -= window.innerWidth / 10;
|
||||
yOffset -= window.innerHeight / 10;
|
||||
xOffset *= 1.25;
|
||||
yOffset *= 1.25;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||
if (config.scale > 1) { // zoomed in
|
||||
while (config.scale > 1) {
|
||||
config = scaleSmaller(config);
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (config.scale < 1) {
|
||||
config = scaleBigger(config);
|
||||
}
|
||||
}
|
||||
|
||||
config.scale = 1;
|
||||
|
||||
return config;
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-node-dev",
|
||||
"version": "0.30.0",
|
||||
"version": "0.33.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -60,8 +60,8 @@
|
|||
"change-case": "^4.1.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
"inquirer": "^7.0.1",
|
||||
"n8n-core": "~0.90.0",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-core": "~0.93.0",
|
||||
"n8n-workflow": "~0.76.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"request": "^2.88.2",
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class DropcontactApi implements ICredentialType {
|
||||
name = 'dropcontactApi';
|
||||
displayName = 'Dropcontact API';
|
||||
documentationUrl = 'dropcontact';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class MicrosoftDynamicsOAuth2Api implements ICredentialType {
|
||||
name = 'microsoftDynamicsOAuth2Api';
|
||||
extends = [
|
||||
'microsoftOAuth2Api',
|
||||
];
|
||||
displayName = 'Microsoft Dynamics OAuth2 API';
|
||||
documentationUrl = 'microsoft';
|
||||
properties: INodeProperties[] = [
|
||||
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
|
||||
{
|
||||
displayName: 'Subdomain',
|
||||
name: 'subdomain',
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'organization',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden',
|
||||
default: '=openid offline_access https://{{$self.subdomain}}.crm.dynamics.com/.default',
|
||||
},
|
||||
];
|
||||
}
|
19
packages/nodes-base/credentials/OneSimpleApi.credentials.ts
Normal file
19
packages/nodes-base/credentials/OneSimpleApi.credentials.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class OneSimpleApi implements ICredentialType {
|
||||
name = 'oneSimpleApi';
|
||||
displayName = 'One Simple API';
|
||||
documentationUrl = 'oneSimpleApi';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'API Token',
|
||||
name: 'apiToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -15,6 +15,8 @@ const userScopes = [
|
|||
'reactions:write',
|
||||
'stars:read',
|
||||
'stars:write',
|
||||
'usergroups:write',
|
||||
'usergroups:read',
|
||||
'users.profile:read',
|
||||
'users.profile:write',
|
||||
];
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.activationTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Core Nodes"
|
||||
],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.activationTrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.json
Normal file
20
packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.awsTextract",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Utility"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/aws"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsTextract/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
packages/nodes-base/nodes/Dropcontact/Dropcontact.node.json
Normal file
20
packages/nodes-base/nodes/Dropcontact/Dropcontact.node.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.dropcontact",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Sales"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/dropcontact"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.dropcontact/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
370
packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts
Normal file
370
packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts
Normal file
|
@ -0,0 +1,370 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialTestFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeApiError,
|
||||
NodeCredentialTestResult,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
dropcontactApiRequest,
|
||||
validateCrendetials,
|
||||
} from './GenericFunction';
|
||||
|
||||
export class Dropcontact implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Dropcontact',
|
||||
name: 'dropcontact',
|
||||
icon: 'file:dropcontact.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Find B2B emails and enrich contacts',
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
defaults: {
|
||||
name: 'Dropcontact',
|
||||
color: '#0ABA9F',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'dropcontactApi',
|
||||
required: true,
|
||||
testedBy: 'dropcontactApiCredentialTest',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
noDataExpression: true,
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Contact',
|
||||
value: 'contact',
|
||||
},
|
||||
],
|
||||
default: 'contact',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
noDataExpression: true,
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Enrich',
|
||||
value: 'enrich',
|
||||
description: 'Find B2B emails and enrich your contact from his name and his website',
|
||||
},
|
||||
{
|
||||
name: 'Fetch Request',
|
||||
value: 'fetchRequest',
|
||||
},
|
||||
],
|
||||
default: 'enrich',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Request ID',
|
||||
name: 'requestId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'fetchRequest',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'enrich',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output (Faster)',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'enrich',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'When off, waits for the contact data before completing. Waiting time can be adjusted with Extend Wait Time option. When on, returns a request_id that can be used later in the Fetch Request operation.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'enrich',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Company SIREN Number',
|
||||
name: 'num_siren',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Company SIRET Code',
|
||||
name: 'siret',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Company Name',
|
||||
name: 'company',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Full Name',
|
||||
name: 'full_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'LinkedIn Profile',
|
||||
name: 'linkedin',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone Number',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'enrich',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Data Fetch Wait Time',
|
||||
name: 'waitTime',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/simplify': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: 45,
|
||||
description: 'When not simplifying the response, data will be fetched in two steps. This parameter controls how long to wait (in seconds) before trying the second step',
|
||||
},
|
||||
{
|
||||
displayName: 'French Company Enrich',
|
||||
name: 'siren',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `Whether you want the <a href="https://en.wikipedia.org/wiki/SIREN_code" target="_blank">SIREN number</a>, NAF code, TVA number, company address and informations about the company leader.</br>
|
||||
Only applies to french companies`,
|
||||
},
|
||||
{
|
||||
displayName: 'Language',
|
||||
name: 'language',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'fr',
|
||||
},
|
||||
],
|
||||
default: 'en',
|
||||
description: 'Whether the response is in English or French',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
|
||||
try {
|
||||
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: 'The API Key included in the request is invalid',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Connection successful!',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const entryData = this.getInputData();
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
// tslint:disable-next-line: no-any
|
||||
let responseData: any;
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
if (resource === 'contact') {
|
||||
if (operation === 'enrich') {
|
||||
const options = this.getNodeParameter('options', 0) as IDataObject;
|
||||
const data = [];
|
||||
const simplify = this.getNodeParameter('simplify', 0) as boolean;
|
||||
|
||||
const siren = options.siren === true ? true : false;
|
||||
const language = options.language ? options.language : 'en';
|
||||
|
||||
for (let i = 0; i < entryData.length; i++) {
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const body: IDataObject = {};
|
||||
if (email !== '') {
|
||||
body.email = email;
|
||||
}
|
||||
Object.assign(body, additionalFields);
|
||||
data.push(body);
|
||||
}
|
||||
|
||||
responseData = await dropcontactApiRequest.call(this, 'POST', '/batch', { data, siren, language }, {}) as { request_id: string, error: string, success: boolean };
|
||||
|
||||
if (!responseData.success) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ error: responseData.reason || 'invalid request' });
|
||||
} else {
|
||||
throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' });
|
||||
}
|
||||
}
|
||||
|
||||
if (simplify === false) {
|
||||
const waitTime = this.getNodeParameter('options.waitTime', 0, 45) as number;
|
||||
// tslint:disable-next-line: no-any
|
||||
const delay = (ms: any) => new Promise(res => setTimeout(res, ms * 1000));
|
||||
await delay(waitTime);
|
||||
responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${responseData.request_id}`, {}, {});
|
||||
if (!responseData.success) {
|
||||
if (this.continueOnFail()) {
|
||||
responseData.push({ error: responseData.reason });
|
||||
} else {
|
||||
throw new NodeApiError(this.getNode(), {
|
||||
error: responseData.reason,
|
||||
description: 'Hint: Increase the Wait Time to avoid this error',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
returnData.push(...responseData.data);
|
||||
}
|
||||
} else {
|
||||
returnData.push(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'fetchRequest') {
|
||||
for (let i = 0; i < entryData.length; i++) {
|
||||
const requestId = this.getNodeParameter('requestId', i) as string;
|
||||
responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${requestId}`, {}, {}) as { request_id: string, error: string, success: boolean };
|
||||
if (!responseData.success) {
|
||||
if (this.continueOnFail()) {
|
||||
responseData.push({ error: responseData.reason || 'invalid request' });
|
||||
} else {
|
||||
throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' });
|
||||
}
|
||||
}
|
||||
returnData.push(...responseData.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
82
packages/nodes-base/nodes/Dropcontact/GenericFunction.ts
Normal file
82
packages/nodes-base/nodes/Dropcontact/GenericFunction.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
NodeApiError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
/**
|
||||
* Make an authenticated API request to Bubble.
|
||||
*/
|
||||
export async function dropcontactApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body: IDataObject,
|
||||
qs: IDataObject,
|
||||
) {
|
||||
|
||||
const { apiKey } = await this.getCredentials('dropcontactApi') as {
|
||||
apiKey: string,
|
||||
};
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'user-agent': 'n8n',
|
||||
'X-Access-Token': apiKey,
|
||||
},
|
||||
method,
|
||||
uri: `https://api.dropcontact.io${endpoint}`,
|
||||
qs,
|
||||
body,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (!Object.keys(qs).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateCrendetials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = decryptedCredentials;
|
||||
|
||||
const { apiKey } = credentials as {
|
||||
apiKey: string,
|
||||
};
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'user-agent': 'n8n',
|
||||
'X-Access-Token': apiKey,
|
||||
},
|
||||
method: 'POST',
|
||||
body: {
|
||||
data: [{ email: '' }],
|
||||
},
|
||||
uri: `https://api.dropcontact.io/batch`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
return this.helpers.request!(options);
|
||||
}
|
||||
|
3
packages/nodes-base/nodes/Dropcontact/dropcontact.svg
Normal file
3
packages/nodes-base/nodes/Dropcontact/dropcontact.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5692 16.219C28.1469 15.5371 26.5535 15.1552 24.8709 15.1552C18.8615 15.1552 13.9899 20.0268 13.9899 26.0362C13.9899 32.0456 18.8615 36.9172 24.8709 36.9172C28.9621 36.9172 32.526 34.6592 34.3843 31.3215L31.7614 29.8643C30.4154 32.2818 27.8341 33.9172 24.8709 33.9172C20.5183 33.9172 16.9899 30.3888 16.9899 26.0362C16.9899 21.6836 20.5183 18.1552 24.8709 18.1552C26.0896 18.1552 27.2437 18.4318 28.2738 18.9257L29.5692 16.219ZM34.7779 1.98444V27.7461H31.669V0.899363C29.5459 0.313171 27.3095 0 25 0C11.1929 0 0 11.1929 0 25C0 38.8071 11.1929 50 25 50C38.8071 50 50 38.8071 50 25C50 14.6626 43.7259 5.7907 34.7779 1.98444Z" fill="#0ABA9F"/>
|
||||
</svg>
|
After Width: | Height: | Size: 773 B |
|
@ -152,6 +152,10 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Circle',
|
||||
value: 'circle',
|
||||
},
|
||||
{
|
||||
name: 'Line',
|
||||
value: 'line',
|
||||
|
@ -192,6 +196,7 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
'draw',
|
||||
],
|
||||
primitive: [
|
||||
'circle',
|
||||
'line',
|
||||
'rectangle',
|
||||
],
|
||||
|
@ -210,6 +215,7 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
'draw',
|
||||
],
|
||||
primitive: [
|
||||
'circle',
|
||||
'line',
|
||||
'rectangle',
|
||||
],
|
||||
|
@ -228,6 +234,7 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
'draw',
|
||||
],
|
||||
primitive: [
|
||||
'circle',
|
||||
'line',
|
||||
'rectangle',
|
||||
],
|
||||
|
@ -246,6 +253,7 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
'draw',
|
||||
],
|
||||
primitive: [
|
||||
'circle',
|
||||
'line',
|
||||
'rectangle',
|
||||
],
|
||||
|
@ -472,6 +480,110 @@ const nodeOperationOptions: INodeProperties[] = [
|
|||
},
|
||||
description: 'The name of the binary property which contains the data of the image to composite on top of image which is found in Property Name.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operator',
|
||||
name: 'operator',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'composite',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'Add',
|
||||
},
|
||||
{
|
||||
name: 'Atop',
|
||||
value: 'Atop',
|
||||
},
|
||||
{
|
||||
name: 'Bumpmap',
|
||||
value: 'Bumpmap',
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
value: 'Copy',
|
||||
},
|
||||
{
|
||||
name: 'Copy Black',
|
||||
value: 'CopyBlack',
|
||||
},
|
||||
{
|
||||
name: 'Copy Blue',
|
||||
value: 'CopyBlue',
|
||||
},
|
||||
{
|
||||
name: 'Copy Cyan',
|
||||
value: 'CopyCyan',
|
||||
},
|
||||
{
|
||||
name: 'Copy Green',
|
||||
value: 'CopyGreen',
|
||||
},
|
||||
{
|
||||
name: 'Copy Magenta',
|
||||
value: 'CopyMagenta',
|
||||
},
|
||||
{
|
||||
name: 'Copy Opacity',
|
||||
value: 'CopyOpacity',
|
||||
},
|
||||
{
|
||||
name: 'Copy Red',
|
||||
value: 'CopyRed',
|
||||
},
|
||||
{
|
||||
name: 'Copy Yellow',
|
||||
value: 'CopyYellow',
|
||||
},
|
||||
{
|
||||
name: 'Difference',
|
||||
value: 'Difference',
|
||||
},
|
||||
{
|
||||
name: 'Divide',
|
||||
value: 'Divide',
|
||||
},
|
||||
{
|
||||
name: 'In',
|
||||
value: 'In',
|
||||
},
|
||||
{
|
||||
name: 'Minus',
|
||||
value: 'Minus',
|
||||
},
|
||||
{
|
||||
name: 'Multiply',
|
||||
value: 'Multiply',
|
||||
},
|
||||
{
|
||||
name: 'Out',
|
||||
value: 'Out',
|
||||
},
|
||||
{
|
||||
name: 'Over',
|
||||
value: 'Over',
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
value: 'Plus',
|
||||
},
|
||||
{
|
||||
name: 'Subtract',
|
||||
value: 'Subtract',
|
||||
},
|
||||
{
|
||||
name: 'Xor',
|
||||
value: 'Xor',
|
||||
},
|
||||
],
|
||||
default: 'Over',
|
||||
description: 'The operator to use to combine the images.',
|
||||
},
|
||||
{
|
||||
displayName: 'Position X',
|
||||
name: 'positionX',
|
||||
|
@ -1095,6 +1207,7 @@ export class EditImage implements INodeType {
|
|||
} else if (operationData.operation === 'composite') {
|
||||
const positionX = operationData.positionX as number;
|
||||
const positionY = operationData.positionY as number;
|
||||
const operator = operationData.operator as string;
|
||||
|
||||
const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY;
|
||||
|
||||
|
@ -1109,9 +1222,9 @@ export class EditImage implements INodeType {
|
|||
if (operations[0].operation === 'create') {
|
||||
// It seems like if the image gets created newly we have to create a new gm instance
|
||||
// else it fails for some reason
|
||||
gmInstance = gm(gmInstance!.stream('png')).geometry(geometryString).composite(path);
|
||||
gmInstance = gm(gmInstance!.stream('png')).compose(operator).geometry(geometryString).composite(path);
|
||||
} else {
|
||||
gmInstance = gmInstance!.geometry(geometryString).composite(path);
|
||||
gmInstance = gmInstance!.compose(operator).geometry(geometryString).composite(path);
|
||||
}
|
||||
|
||||
if (operations.length !== i + 1) {
|
||||
|
@ -1131,6 +1244,8 @@ export class EditImage implements INodeType {
|
|||
|
||||
if (operationData.primitive === 'line') {
|
||||
gmInstance = gmInstance.drawLine(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number);
|
||||
} else if (operationData.primitive === 'circle') {
|
||||
gmInstance = gmInstance.drawCircle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number);
|
||||
} else if (operationData.primitive === 'rectangle') {
|
||||
gmInstance = gmInstance.drawRectangle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number, operationData.cornerRadius as number || undefined);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.googleDriveTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Data & Storage"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/google"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleDriveTrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -124,7 +124,7 @@ export class GoogleTasks implements INodeType {
|
|||
body.notes = additionalFields.notes as string;
|
||||
}
|
||||
if (additionalFields.dueDate) {
|
||||
body.dueDate = additionalFields.dueDate as string;
|
||||
body.due = additionalFields.dueDate as string;
|
||||
}
|
||||
|
||||
if (additionalFields.completed) {
|
||||
|
@ -249,7 +249,7 @@ export class GoogleTasks implements INodeType {
|
|||
}
|
||||
|
||||
if (updateFields.dueDate) {
|
||||
body.dueDate = updateFields.dueDate as string;
|
||||
body.due = updateFields.dueDate as string;
|
||||
}
|
||||
|
||||
if (updateFields.completed) {
|
||||
|
|
|
@ -447,6 +447,13 @@ export const taskFields = [
|
|||
default: false,
|
||||
description: 'Flag indicating whether the task has been deleted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Due date of the task.',
|
||||
},
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue