n8n/packages/editor-ui/src/components/mixins/workflowHelpers.ts
Mutasem Aldmour d8598b0126
Workflow canvas revamp (#2388)
* bring back overrides

* fix input output label positions

* simply update label positions

* refactor a bunch

* update min x to show items

* hide overlay on connection

* only delete target connection, add maximum to push nodes out

* rename const

* rename const

* set new insert position

* fix insert behavior

* update position handling

* show arrow along with label

* update connector

* set endpoint styles

* update pattern

* push nodes up / down in case of if node

* set position in switch

* only one action at a time

* add custom flow chart type

* select start node by default when opening new workflow

* add enter delay

* fix delete bug

* change connection type

* add offset for if/switch/merge

* fix gap

* fix drag issue

* implement new states

* update disabled state

* add selected state

* make selects faster

* update positioning

* truncate when selected

* remove offset for actions

* fix icon scaling

* refactor js plumb

* fix looping behavior at close distance

* lock version

* change background to dots

* update endpoints styling

* increase spacing

* udpate node z-index

* fix output label positions

* fix output label positions

* reset location

* add label offset

* update border radius

* fix height issue

* fix parallaxing issue

* fix zoomout issue

* add success z-index

* clean up js file

* add package lock

* fix z-index bug

* update dot grid

* update zoom level

* set values, increase grid size

* fix drop position

* prevent duplicate connections

* fix stub

* use localstorage overrides for colors

* add colors to system

* revert no longer needed changes

* revert no longer needed changes

* add canvas colors

* add canvas colors

* use variable for id

* force type

* refactor helpers

* add label constants

* refactor func

* refactor

* fix

* refactor

* clean up css

* refactor setzoom level

* refactor

* refactor

* refactor func

* remove scope

* remove localstorage caching

* clean up imports

* update zero case

* add delete connection

* update selected state

* add base type, remove straight line

* add stub offset back

* rename param

* add label offset

* update font size of items

* move up label

* fix error state while executing

* disrespect stubs

* check for errors

* refactor position

* clean up extra space

* make entire node connectable

* Revert "make entire node connectable"

e304f7c5b8

* always show border

* add border to zoom buttons

* update spacing

* update colors

* allow connecting to entire node

* fix pull conn active

* two line names

* apply select to all lines

* increase input margin

* override target pos

* reset conn after pull

* fix types

* update orientation

* fix up connectors snapping

* hide arrow on pull

* update overrides for connectors

* change text

* update pull colors

* set to 1 line when selected

* fix executions bug

* build

* refactor node component

* remove comment

* refactor more

* remove prop

* fix build issue

* fix input drag bug in executions

* reset offset

* update select background

* handle issue when endpoints are not set

* fix connection aborted issue

* add try catch to help show errors

* wrap bind with try/catch

* set default styles

* reset pos despite zoom

* add more checks

* clean up impl

* update icon

* handle unknown types

* hide items on init

* fix importing unknown types with credentials

* change opacity

* push up item label

* update color

* update label class and colors

* add to drop distance

* fix z-index to match node

* disable eslint

* fix lasso tool selection

* update background color

* update waiting state

* update tooltip positions

* update wait node border

* fix selection bug mostly

* if selected, move above other nodes

* add line through disabled nodes

* remove node color option

* move label above connection

* success color for line through

* update options index

* hide waiting icon when disabled

* fix gmail icon

* refactor icons

* clear execution data on disable/delete

* fix selected node

* fix executing behavior

* optional __meta

* set grid size

* remove default color

* remove node color

* add comments

* comments

* add comments

* remove empty space

* update comment

* refactor uuids

* fix type issue

* Revert "fix type issue"

9523b34f96

* Revert "fix type issue"

9523b34f96

* Revert "refactor uuids"

07f6848065

* fix build issues

* refactor

* update uuid

* child nodes

* skip nodes behind when pushing in loop

* shift output icon for switch node

* don't show output if waiting

* waiting on init

* build

* change to bezier

* revert connector change

* add bezier type

* fix snapping

* clean up impl

* refactor func

* make const

* rename type

* refactor to simplify

* Revert "refactor to simplify"

2db0ed504c

* enable flowchart mode

* clean up flowchart type

* refactor type

* merge types

* configure curviness

* set in localstorage

* fix straight line arrow bug

* show arrow when pulling

* refactor / simplify

* fix target gap in bezier

* refactor target gap

* add comments

* add comment

* fix dragging connections

* fix bug when moving connection

* update comment

* rename file

* update values

* update minor

* update straight line box

* clean up conn types

* clean up z-indexes

* move color filters to node icon

* update background color

* update to use grid size value

* fix endpoint offsets

* set yspan range lower

* remove overlays when moving conn

* prevent unwanted connections

* fix messed up connections

* remove console log

* clear execution issues on workflow run

* update corner radius

* fix drag/delete bug

* increase offset

* update disabled state

* address comments

* refactor

* refactor func

*  Add full license text to N8nCustomConnectorType.js

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-11-19 10:17:13 +01:00

618 lines
19 KiB
TypeScript

import {
ERROR_TRIGGER_NODE_TYPE,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
START_NODE_TYPE,
WEBHOOK_NODE_TYPE,
} from '@/constants';
import {
IConnections,
IDataObject,
INode,
INodeExecutionData,
INodeIssues,
INodeParameters,
NodeParameterValue,
INodeCredentials,
INodeType,
INodeTypes,
INodeTypeData,
INodeTypeDescription,
INodeVersionedType,
IRunData,
IRunExecutionData,
IWorfklowIssues,
IWorkflowDataProxyAdditionalKeys,
TelemetryHelpers,
Workflow,
NodeHelpers,
} from 'n8n-workflow';
import {
IExecutionResponse,
INodeTypesMaxCount,
INodeUi,
IWorkflowData,
IWorkflowDb,
IWorkflowDataUpdate,
XYPosition,
ITag,
IUpdateInformation,
} from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { isEqual } from 'lodash';
import mixins from 'vue-typed-mixins';
import { v4 as uuidv4} from 'uuid';
export const workflowHelpers = mixins(
externalHooks,
nodeHelpers,
restApi,
showMessage,
)
.extend({
methods: {
// Returns connectionInputData to be able to execute an expression.
connectionInputData (parentNode: string[], inputName: string, runIndex: number, inputIndex: number): INodeExecutionData[] | null {
let connectionInputData = null;
if (parentNode.length) {
// Add the input data to be able to also resolve the short expression format
// which does not use the node name
const parentNodeName = parentNode[0];
const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null;
if (workflowRunData === null) {
return null;
}
if (!workflowRunData[parentNodeName] ||
workflowRunData[parentNodeName].length <= runIndex ||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
workflowRunData[parentNodeName][runIndex].data === undefined ||
!workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName) ||
workflowRunData[parentNodeName][runIndex].data![inputName].length <= inputIndex
) {
connectionInputData = [];
} else {
connectionInputData = workflowRunData[parentNodeName][runIndex].data![inputName][inputIndex];
}
}
return connectionInputData;
},
// Returns a shallow copy of the nodes which means that all the data on the lower
// levels still only gets referenced but the top level object is a different one.
// This has the advantage that it is very fast and does not cause problems with vuex
// when the workflow replaces the node-parameters.
getNodes (): INodeUi[] {
const nodes = this.$store.getters.allNodes;
const returnNodes: INodeUi[] = [];
for (const node of nodes) {
returnNodes.push(Object.assign({}, node));
}
return returnNodes;
},
// Returns data about nodeTypes which ahve a "maxNodes" limit set.
// For each such type does it return how high the limit is, how many
// already exist and the name of this nodes.
getNodeTypesMaxCount (): INodeTypesMaxCount {
const nodes = this.$store.getters.allNodes;
const returnData: INodeTypesMaxCount = {};
const nodeTypes = this.$store.getters.allNodeTypes;
for (const nodeType of nodeTypes) {
if (nodeType.maxNodes !== undefined) {
returnData[nodeType.name] = {
exist: 0,
max: nodeType.maxNodes,
nodeNames: [],
};
}
}
for (const node of nodes) {
if (returnData[node.type] !== undefined) {
returnData[node.type].exist += 1;
returnData[node.type].nodeNames.push(node.name);
}
}
return returnData;
},
// Returns how many nodes of the given type currently exist
getNodeTypeCount (nodeType: string): number {
const nodes = this.$store.getters.allNodes;
let count = 0;
for (const node of nodes) {
if (node.type === nodeType) {
count++;
}
}
return count;
},
// Checks if everything in the workflow is complete and ready to be executed
checkReadyForExecution (workflow: Workflow, lastNodeName?: string) {
let node: INode;
let nodeType: INodeType | undefined;
let nodeIssues: INodeIssues | null = null;
const workflowIssues: IWorfklowIssues = {};
let checkNodes = Object.keys(workflow.nodes);
if (lastNodeName) {
checkNodes = workflow.getParentNodes(lastNodeName);
checkNodes.push(lastNodeName);
} else {
// As webhook nodes always take presidence check first
// if there are any
let checkWebhook: string[] = [];
for (const nodeName of Object.keys(workflow.nodes)) {
if (workflow.nodes[nodeName].disabled !== true && workflow.nodes[nodeName].type === WEBHOOK_NODE_TYPE) {
checkWebhook = [nodeName, ...checkWebhook, ...workflow.getChildNodes(nodeName)];
}
}
if (checkWebhook.length) {
checkNodes = checkWebhook;
} else {
// If no webhook nodes got found try to find another trigger node
const startNode = workflow.getStartNode();
if (startNode !== undefined) {
checkNodes = workflow.getChildNodes(startNode.name);
checkNodes.push(startNode.name);
}
}
}
for (const nodeName of checkNodes) {
nodeIssues = null;
node = workflow.nodes[nodeName];
if (node.disabled === true) {
// Ignore issues on disabled nodes
continue;
}
nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
// Node type is not known
nodeIssues = {
typeUnknown: true,
};
} else {
nodeIssues = this.getNodeIssues(nodeType.description, node, ['execution']);
}
if (nodeIssues !== null) {
workflowIssues[node.name] = nodeIssues;
}
}
if (Object.keys(workflowIssues).length === 0) {
return null;
}
return workflowIssues;
},
// Returns a workflow instance.
getWorkflow (nodes?: INodeUi[], connections?: IConnections, copyData?: boolean): Workflow {
nodes = nodes || this.getNodes();
connections = connections || (this.$store.getters.allConnections as IConnections);
const nodeTypes: INodeTypes = {
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
getAll: (): Array<INodeType | INodeVersionedType> => {
// Does not get used in Workflow so no need to return it
return [];
},
getByName: (nodeType: string): INodeType | INodeVersionedType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType) as INodeTypeDescription | null;
if (nodeTypeDescription === null) {
return undefined;
}
return {
description: nodeTypeDescription,
};
},
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version) as INodeTypeDescription | null;
if (nodeTypeDescription === null) {
return undefined;
}
return {
description: nodeTypeDescription,
// As we do not have the trigger/poll functions available in the frontend
// we use the information available to figure out what are trigger nodes
// @ts-ignore
trigger: ![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && nodeTypeDescription.inputs.length === 0 && !nodeTypeDescription.webhooks || undefined,
};
},
};
let workflowId = this.$store.getters.workflowId;
if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
workflowId = undefined;
}
const workflowName = this.$store.getters.workflowName;
if (copyData === true) {
return new Workflow({ id: workflowId, name: workflowName, nodes: JSON.parse(JSON.stringify(nodes)), connections: JSON.parse(JSON.stringify(connections)), active: false, nodeTypes});
} else {
return new Workflow({ id: workflowId, name: workflowName, nodes, connections, active: false, nodeTypes});
}
},
// Returns the currently loaded workflow as JSON.
getWorkflowDataToSave (): Promise<IWorkflowData> {
const workflowNodes = this.$store.getters.allNodes;
const workflowConnections = this.$store.getters.allConnections;
let nodeData;
const nodes = [];
for (let nodeIndex = 0; nodeIndex < workflowNodes.length; nodeIndex++) {
try {
// @ts-ignore
nodeData = this.getNodeDataToSave(workflowNodes[nodeIndex]);
} catch (e) {
return Promise.reject(e);
}
nodes.push(nodeData);
}
const data: IWorkflowData = {
name: this.$store.getters.workflowName,
nodes,
connections: workflowConnections,
active: this.$store.getters.isActive,
settings: this.$store.getters.workflowSettings,
tags: this.$store.getters.workflowTags,
};
const workflowId = this.$store.getters.workflowId;
if (workflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
data.id = workflowId;
}
return Promise.resolve(data);
},
// Returns all node-types
getNodeDataToSave (node: INodeUi): INodeUi {
const skipKeys = [
'color',
'continueOnFail',
'credentials',
'disabled',
'issues',
'notes',
'parameters',
'status',
];
// @ts-ignore
const nodeData: INodeUi = {
parameters: {},
};
for (const key in node) {
if (key.charAt(0) !== '_' && skipKeys.indexOf(key) === -1) {
// @ts-ignore
nodeData[key] = node[key];
}
}
// 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 | null;
if (nodeType !== null) {
// Node-Type is known so we can save the parameters correctly
const nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
nodeData.parameters = nodeParameters !== null ? nodeParameters : {};
// Add the node credentials if there are some set and if they should be displayed
if (node.credentials !== undefined && nodeType.credentials !== undefined) {
const saveCredenetials: INodeCredentials = {};
for (const nodeCredentialTypeName of Object.keys(node.credentials)) {
const credentialTypeDescription = nodeType.credentials
.find((credentialTypeDescription) => credentialTypeDescription.name === nodeCredentialTypeName);
if (credentialTypeDescription === undefined) {
// Credential type is not know so do not save
continue;
}
if (this.displayParameter(node.parameters, credentialTypeDescription, '') === false) {
// Credential should not be displayed so do also not save
continue;
}
saveCredenetials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName];
}
// Set credential property only if it has content
if (Object.keys(saveCredenetials).length !== 0) {
nodeData.credentials = saveCredenetials;
}
}
} else {
// Node-Type is not known so save the data as it is
nodeData.credentials = node.credentials;
nodeData.parameters = node.parameters;
if (nodeData.color !== undefined) {
nodeData.color = node.color;
}
}
// Save the disabled property and continueOnFail only when is set
if (node.disabled === true) {
nodeData.disabled = true;
}
if (node.continueOnFail === true) {
nodeData.continueOnFail = true;
}
// Save the notes only if when they contain data
if (![undefined, ''].includes(node.notes)) {
nodeData.notes = node.notes;
}
return nodeData;
},
resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) {
const inputIndex = 0;
const itemIndex = 0;
const runIndex = 0;
const inputName = 'main';
const activeNode = this.$store.getters.activeNode;
const workflow = this.getWorkflow();
const parentNode = workflow.getParentNodes(activeNode.name, inputName, 1);
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
let connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex);
let runExecutionData: IRunExecutionData;
if (executionData === null) {
runExecutionData = {
resultData: {
runData: {},
},
};
} else {
runExecutionData = executionData.data;
}
if (connectionInputData === null) {
connectionInputData = [];
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', additionalKeys, false) as IDataObject;
},
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {
const parameters = {
'__xxxxxxx__': expression,
...siblingParameters,
};
const returnData = this.resolveParameter(parameters) as IDataObject;
if (typeof returnData['__xxxxxxx__'] === 'object') {
const workflow = this.getWorkflow();
return workflow.expression.convertObjectValueToString(returnData['__xxxxxxx__'] as object);
}
return returnData['__xxxxxxx__'];
},
async saveCurrentWorkflow({name, tags}: {name?: string, tags?: string[]} = {}): Promise<boolean> {
const currentWorkflow = this.$route.params.name;
if (!currentWorkflow) {
return this.saveAsNewWorkflow({name, tags});
}
// Workflow exists already so update it
try {
this.$store.commit('addActiveAction', 'workflowSaving');
const workflowDataRequest: IWorkflowDataUpdate = await this.getWorkflowDataToSave();
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
const workflowData = await this.restApi().updateWorkflow(currentWorkflow, workflowDataRequest);
if (name) {
this.$store.commit('setWorkflowName', {newName: workflowData.name});
}
if (tags) {
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
this.$store.commit('setWorkflowTagIds', tagIds);
}
this.$store.commit('setStateDirty', false);
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
return true;
} catch (e) {
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$showMessage({
title: 'Problem saving workflow',
message: `There was a problem saving the workflow: "${e.message}"`,
type: 'error',
});
return false;
}
},
async saveAsNewWorkflow ({name, tags, resetWebhookUrls, openInNewWindow}: {name?: string, tags?: string[], resetWebhookUrls?: boolean, openInNewWindow?: boolean} = {}): Promise<boolean> {
try {
this.$store.commit('addActiveAction', 'workflowSaving');
const workflowDataRequest: IWorkflowDataUpdate = await this.getWorkflowDataToSave();
// make sure that the new ones are not active
workflowDataRequest.active = false;
const changedNodes = {} as IDataObject;
if (resetWebhookUrls) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map(node => {
if (node.webhookId) {
node.webhookId = uuidv4();
changedNodes[node.name] = node.webhookId;
}
return node;
});
}
if (name) {
workflowDataRequest.name = name.trim();
}
if (tags) {
workflowDataRequest.tags = tags;
}
const workflowData = await this.restApi().createNewWorkflow(workflowDataRequest);
if (openInNewWindow) {
const routeData = this.$router.resolve({name: 'NodeViewExisting', params: {name: workflowData.id}});
window.open(routeData.href, '_blank');
this.$store.commit('removeActiveAction', 'workflowSaving');
return true;
}
this.$store.commit('setActive', workflowData.active || false);
this.$store.commit('setWorkflowId', workflowData.id);
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
this.$store.commit('setStateDirty', false);
Object.keys(changedNodes).forEach((nodeName) => {
const changes = {
key: 'webhookId',
value: changedNodes[nodeName],
name: nodeName,
} as IUpdateInformation;
this.$store.commit('setNodeValue', changes);
});
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
this.$store.commit('setWorkflowTagIds', tagIds);
this.$router.push({
name: 'NodeViewExisting',
params: { name: workflowData.id as string, action: 'workflowSave' },
});
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$store.commit('setStateDirty', false);
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
return true;
} catch (e) {
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$showMessage({
title: 'Problem saving workflow',
message: `There was a problem saving the workflow: "${e.message}"`,
type: 'error',
});
return false;
}
},
// Updates the position of all the nodes that the top-left node
// is at the given position
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPosition): void {
if (workflowData.nodes === undefined) {
return;
}
// Find most top-left node
const minPosition = [99999999, 99999999];
for (const node of workflowData.nodes) {
if (node.position[1] < minPosition[1]) {
minPosition[0] = node.position[0];
minPosition[1] = node.position[1];
} else if (node.position[1] === minPosition[1]) {
if (node.position[0] < minPosition[0]) {
minPosition[0] = node.position[0];
minPosition[1] = node.position[1];
}
}
}
// Update the position on all nodes so that the
// most top-left one is at given position
const offsetPosition = [position[0] - minPosition[0], position[1] - minPosition[1]];
for (const node of workflowData.nodes) {
node.position[0] += offsetPosition[0];
node.position[1] += offsetPosition[1];
}
},
async dataHasChanged(id: string) {
const currentData = await this.getWorkflowDataToSave();
const data: IWorkflowDb = await this.restApi().getWorkflow(id);
if(data !== undefined) {
const x = {
nodes: data.nodes,
connections: data.connections,
settings: data.settings,
name: data.name,
};
const y = {
nodes: currentData.nodes,
connections: currentData.connections,
settings: currentData.settings,
name: currentData.name,
};
return !isEqual(x, y);
}
return true;
},
},
});