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"

e304f7c5b8ff1b41268450c60ca4bc3b2ada5d4a

* 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"

9523b34f9604f75253ae0631f29fc27267a99d78

* Revert "fix type issue"

9523b34f9604f75253ae0631f29fc27267a99d78

* Revert "refactor uuids"

07f6848065cb9a98475fddb8330846106f9e70ad

* 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"

2db0ed504c752c33de975370d86a83a04ffcda14

* 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>
This commit is contained in:
Mutasem Aldmour
2021-11-19 10:17:13 +01:00
committed by GitHub
parent 0c6af9fd95
commit d8598b0126
26 changed files with 2720 additions and 1062 deletions

View File

@@ -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;
},
},
});

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -191,6 +191,7 @@ export const workflowRun = mixins(
},
};
this.$store.commit('setWorkflowExecutionData', executionData);
this.updateNodesExecutionIssues();
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);