fix(editor): Fix for missing node connections in dev environment (#4707)

* 🐛 Fixing connections not showing up in dev environment
* 🐛 Fixing a bug when opening execution page directly
This commit is contained in:
Milorad FIlipović 2022-11-24 10:52:56 +01:00 committed by GitHub
parent ee6ac5d341
commit b18ae18a6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 810 additions and 843 deletions

View file

@ -18,7 +18,7 @@
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from "vue";
import { getMidCanvasPosition } from '@/utils'; import { getMidCanvasPosition } from '@/utils/nodeViewUtils';
import {DEFAULT_STICKY_HEIGHT, DEFAULT_STICKY_WIDTH, STICKY_NODE_TYPE} from "@/constants"; import {DEFAULT_STICKY_HEIGHT, DEFAULT_STICKY_WIDTH, STICKY_NODE_TYPE} from "@/constants";
import { mapStores } from "pinia"; import { mapStores } from "pinia";
import { useUIStore } from "@/stores/ui"; import { useUIStore } from "@/stores/ui";

View file

@ -58,7 +58,8 @@
import Vue, { PropType } from 'vue'; import Vue, { PropType } from 'vue';
import { INodeTypeDescription } from 'n8n-workflow'; import { INodeTypeDescription } from 'n8n-workflow';
import { isCommunityPackageName, getNewNodePosition, NODE_SIZE } from '@/utils'; import { isCommunityPackageName } from '@/utils';
import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants'; import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
import NodeIcon from '@/components/NodeIcon.vue'; import NodeIcon from '@/components/NodeIcon.vue';

View file

@ -3,11 +3,11 @@ import { INodeUi, XYPosition } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import { getMousePosition, getRelativePosition, HEADER_HEIGHT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_EXPANDED } from '@/utils';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import { getMousePosition, getRelativePosition, HEADER_HEIGHT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_EXPANDED } from '@/utils/nodeViewUtils';
export const mouseSelect = mixins( export const mouseSelect = mixins(
deviceSupportHelpers, deviceSupportHelpers,

View file

@ -1,6 +1,6 @@
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import { getMousePosition } from '@/utils'; import { getMousePosition } from '@/utils/nodeViewUtils';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';

View file

@ -3,17 +3,6 @@ import mixins from 'vue-typed-mixins';
import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface'; import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants'; import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import {
ANCHOR_POSITIONS,
GRID_SIZE,
getInputEndpointUUID,
getOutputEndpointUUID,
getInputEndpointStyle,
getOutputEndpointStyle,
getInputNameOverlay,
getOutputNameOverlay,
getStyleTokenValue,
} from '@/utils';
import { import {
INodeTypeDescription, INodeTypeDescription,
@ -22,6 +11,8 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from "@/stores/workflows"; import { useWorkflowsStore } from "@/stores/workflows";
import { useNodeTypesStore } from "@/stores/nodeTypes"; import { useNodeTypesStore } from "@/stores/nodeTypes";
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { getStyleTokenValue } from "@/utils";
export const nodeBase = mixins( export const nodeBase = mixins(
deviceSupportHelpers, deviceSupportHelpers,
@ -91,15 +82,15 @@ export const nodeBase = mixins(
index = indexData[inputName]; index = indexData[inputName];
// Get the position of the anchor depending on how many it has // Get the position of the anchor depending on how many it has
const anchorPosition = ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index]; const anchorPosition = NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
const newEndpointData: IEndpointOptions = { const newEndpointData: IEndpointOptions = {
uuid: getInputEndpointUUID(this.nodeId, index), uuid:NodeViewUtils. getInputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'Rectangle', endpoint: 'Rectangle',
endpointStyle: getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'), endpointStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointHoverStyle: getInputEndpointStyle(nodeTypeData, '--color-primary'), endpointHoverStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: false, isSource: false,
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView, isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
parameters: { parameters: {
@ -119,7 +110,7 @@ export const nodeBase = mixins(
if (nodeTypeData.inputNames) { if (nodeTypeData.inputNames) {
// Apply input names if they got set // Apply input names if they got set
newEndpointData.overlays = [ newEndpointData.overlays = [
getInputNameOverlay(nodeTypeData.inputNames[index]), NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]),
]; ];
} }
@ -161,15 +152,15 @@ export const nodeBase = mixins(
index = indexData[inputName]; index = indexData[inputName];
// Get the position of the anchor depending on how many it has // Get the position of the anchor depending on how many it has
const anchorPosition = ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index]; const anchorPosition = NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
const newEndpointData: IEndpointOptions = { const newEndpointData: IEndpointOptions = {
uuid: getOutputEndpointUUID(this.nodeId, index), uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'Dot', endpoint: 'Dot',
endpointStyle: getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'), endpointStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointHoverStyle: getOutputEndpointStyle(nodeTypeData, '--color-primary'), endpointHoverStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: true, isSource: true,
isTarget: false, isTarget: false,
enabled: !this.isReadOnly, enabled: !this.isReadOnly,
@ -186,7 +177,7 @@ export const nodeBase = mixins(
if (nodeTypeData.outputNames) { if (nodeTypeData.outputNames) {
// Apply output names if they got set // Apply output names if they got set
newEndpointData.overlays = [ newEndpointData.overlays = [
getOutputNameOverlay(nodeTypeData.outputNames[index]), NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]),
]; ];
} }
@ -202,7 +193,7 @@ export const nodeBase = mixins(
if (!this.isReadOnly) { if (!this.isReadOnly) {
const plusEndpointData: IEndpointOptions = { const plusEndpointData: IEndpointOptions = {
uuid: getOutputEndpointUUID(this.nodeId, index), uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
anchor: anchorPosition, anchor: anchorPosition,
maxConnections: -1, maxConnections: -1,
endpoint: 'N8nPlus', endpoint: 'N8nPlus',
@ -250,7 +241,7 @@ export const nodeBase = mixins(
// https://jsplumb.github.io/jsplumb/home.html // https://jsplumb.github.io/jsplumb/home.html
// Make nodes draggable // Make nodes draggable
this.instance.draggable(this.nodeId, { this.instance.draggable(this.nodeId, {
grid: [GRID_SIZE, GRID_SIZE], grid: [NodeViewUtils.GRID_SIZE, NodeViewUtils.GRID_SIZE],
start: (params: { e: MouseEvent }) => { start: (params: { e: MouseEvent }) => {
if (this.isReadOnly === true) { if (this.isReadOnly === true) {
// Do not allow to move nodes in readOnly mode // Do not allow to move nodes in readOnly mode

View file

@ -8,11 +8,6 @@ import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { INodeUi, XYPosition } from '@/Interface'; import { INodeUi, XYPosition } from '@/Interface';
import { import {
DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
PLACEHOLDER_TRIGGER_NODE_SIZE,
getMidCanvasPosition,
getNewNodePosition,
getZoomToFit,
scaleBigger, scaleBigger,
scaleReset, scaleReset,
scaleSmaller, scaleSmaller,
@ -20,6 +15,7 @@ import {
import { START_NODE_TYPE } from '@/constants'; import { START_NODE_TYPE } from '@/constants';
import '@/plugins/N8nCustomConnectorType'; import '@/plugins/N8nCustomConnectorType';
import '@/plugins/PlusEndpointType'; import '@/plugins/PlusEndpointType';
import { DEFAULT_PLACEHOLDER_TRIGGER_BUTTON, getMidCanvasPosition, getNewNodePosition, getZoomToFit, PLACEHOLDER_TRIGGER_NODE_SIZE } from '@/utils/nodeViewUtils';
export const useCanvasStore = defineStore('canvas', () => { export const useCanvasStore = defineStore('canvas', () => {
const workflowStore = useWorkflowsStore(); const workflowStore = useWorkflowsStore();

View file

@ -282,7 +282,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
} }
this.workflow.name = data.newName; this.workflow.name = data.newName;
if (this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID && this.workflowsById[this.workflow.id]) {
this.workflowsById[this.workflow.id].name = data.newName; this.workflowsById[this.workflow.id].name = data.newName;
} }
}, },

View file

@ -1,275 +1,15 @@
import { isNumber } from "@/utils/typesUtils"; import { MAIN_HEADER_TABS, VIEWS } from "@/constants";
import { getStyleTokenValue } from '@/utils/htmlUtils'; import { IZoomConfig } from "@/Interface";
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME, MAIN_HEADER_TABS, VIEWS } from "@/constants"; import { Route } from "vue-router";
import { EndpointStyle, IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
import { AnchorArraySpec, Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
import {
IConnection,
INode,
ITaskData,
INodeExecutionData,
NodeInputConnections,
INodeTypeDescription,
} from 'n8n-workflow';
/* /*
Constants and utility functions mainly used by canvas store Constants and utility functions mainly used by canvas store
and components used to display workflow in node view. and components used to display workflow in node view.
These are general-purpose functions that are exported
with this module and should be used by importing from
'@/utils'.
*/ */
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 PLACEHOLDER_TRIGGER_NODE_SIZE = 100;
export const DEFAULT_START_POSITION_X = 180;
export const DEFAULT_START_POSITION_Y = 240;
export const HEADER_HEIGHT = 65;
export const SIDEBAR_WIDTH = 65;
export const INNER_SIDEBAR_WIDTH = 310;
export const SIDEBAR_WIDTH_EXPANDED = 200;
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 PLACEHOLDER_BUTTON = 'PlaceholderTriggerButton';
export const DEFAULT_PLACEHOLDER_TRIGGER_BUTTON = {
name: 'Choose a Trigger...',
type: PLACEHOLDER_BUTTON,
typeVersion: 1,
position: [],
parameters: {
height: PLACEHOLDER_TRIGGER_NODE_SIZE,
width: PLACEHOLDER_TRIGGER_NODE_SIZE,
},
};
export const WELCOME_STICKY_NODE = {
name: QUICKSTART_NOTE_NAME,
type: STICKY_NODE_TYPE,
typeVersion: 1,
position: [
0,
0,
] as XYPosition,
parameters: {
height: 300,
width: 380,
},
};
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
cornerRadius: 12,
stub: JSPLUMB_FLOWCHART_STUB + 10,
targetGap: 4,
alwaysRespectStubs: false,
loopbackVerticalLength: NODE_SIZE + GRID_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 totalEndpoints = endpoint && endpoint.__meta ? endpoint.__meta.totalEndpoints : 0;
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
const outputsOffset = totalEndpoints > 3 ? 24 : 0; // avoid intersecting plus
return index * indexOffset + labelOffset + outputsOffset;
},
}];
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 ANCHOR_POSITIONS: {
[key: string]: {
[key: number]: AnchorArraySpec[];
}
} = {
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): EndpointStyle => ({
width: 8,
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
fill: getStyleTokenValue(color),
stroke: getStyleTokenValue(color),
lineWidth: 0,
});
export const getInputNameOverlay = (label: string): OverlaySpec => ([
'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): OverlaySpec => ([
'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) => {
const hasCustomDimensions = [STICKY_NODE_TYPE, PLACEHOLDER_BUTTON].includes(node.type);
const xOffset = hasCustomDimensions && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
const yOffset = hasCustomDimensions && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
const x = node.position[0];
const y = node.position[1];
if (x < accu.minX) {
accu.minX = x;
}
if (y < accu.minY) {
accu.minY = y;
}
if ((x + xOffset) > accu.maxX) {
accu.maxX = x + xOffset;
}
if ((y + yOffset) > accu.maxY) {
accu.maxY = y + yOffset;
}
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 => { export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
scale /= 1.25; scale /= 1.25;
xOffset /= 1.25; xOffset /= 1.25;
@ -313,133 +53,8 @@ export const scaleReset = (config: IZoomConfig): IZoomConfig => {
return config; 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) => { export const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => {
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;
};
function closestNumberDivisibleBy(inputNumber: number, divisibleBy: number) {
const quotient = Math.ceil(inputNumber / divisibleBy); const quotient = Math.ceil(inputNumber / divisibleBy);
// 1st possible closest number // 1st possible closest number
@ -455,334 +70,6 @@ function closestNumberDivisibleBy(inputNumber: number, divisibleBy: number) {
// else inputNumber2 is the required closest number // else inputNumber2 is the required closest number
return inputNumber2; return inputNumber2;
}
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
const targetPosition: XYPosition = [...newPosition];
targetPosition[0] = closestNumberDivisibleBy(targetPosition[0], GRID_SIZE);
targetPosition[1] = closestNumberDivisibleBy(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 getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => {
const { editorWidth, editorHeight } = getContentDimensions();
return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
};
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition, executionPreview: boolean) => {
const squareSize = GRID_SIZE * scale;
const dotSize = 1 * scale;
const dotPosition = (GRID_SIZE / 2) * scale;
if (executionPreview) {
return {
'background-image': 'linear-gradient(135deg, #f9f9fb 25%, #ffffff 25%, #ffffff 50%, #f9f9fb 50%, #f9f9fb 75%, #ffffff 75%, #ffffff 100%)',
'background-size': `${squareSize}px ${squareSize}px`,
'background-position': `left ${offsetPosition[0]}px top ${offsetPosition[1]}px`,
};
}
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 showConnectionActions = (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) => {
const sourceOutputIndex = i;
if (!outputMap[sourceOutputIndex]) {
outputMap[sourceOutputIndex] = {};
}
if (!outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY]) {
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY] = {};
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0] = {
total: 0,
iterations: 0,
};
}
const defaultOutput = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
defaultOutput.total += output ? output.length : 0;
defaultOutput.iterations += output ? 1 : 0;
if (!nodeConnections[sourceOutputIndex]) {
return;
}
nodeConnections[sourceOutputIndex]
.map((connection: IConnection) => {
const targetNodeName = connection.node;
const targetInputIndex = connection.index;
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 getRunItemsLabel = (output: {total: number, iterations: number}): string => {
let label = `${output.total}`;
label = output.total > 1 ? `${label} items` : `${label} item`;
label = output.iterations > 1 ? `${label} total` : label;
return label;
};
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);
}
connection.addOverlay([
'Label',
{
id: OVERLAY_RUN_ITEMS_ID,
label: `<span>${getRunItemsLabel(output)}</span>`,
cssClass: 'connection-run-items-label',
location: .5,
},
]);
showOrHideItemsLabel(connection);
showOrHideMidpointArrow(connection);
};
const getContentDimensions = (): { editorWidth: number, editorHeight: number } => {
let contentWidth = window.innerWidth;
let contentHeight = window.innerHeight;
const nodeViewRoot = document.getElementById('node-view-root');
if (nodeViewRoot) {
const contentBounds = nodeViewRoot.getBoundingClientRect();
contentWidth = contentBounds.width;
contentHeight = contentBounds.height;
}
return {
editorWidth: contentWidth,
editorHeight: contentHeight,
};
};
export const getZoomToFit = (nodes: INodeUi[], addFooterPadding = true): {offset: XYPosition, zoomLevel: number} => {
const { minX, minY, maxX, maxY } = getWorkflowCorners(nodes);
const { editorWidth, editorHeight } = getContentDimensions();
const footerHeight = addFooterPadding ? 200 : 100;
const PADDING = NODE_SIZE * 4;
const diffX = maxX - minX + PADDING;
const scaleX = editorWidth / diffX;
const diffY = maxY - minY + PADDING;
const scaleY = editorHeight / diffY;
const zoomLevel = Math.min(scaleX, scaleY, 1);
let xOffset = (minX * -1) * zoomLevel; // find top right corner
xOffset += (editorWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
let yOffset = (minY * -1) * zoomLevel; // find top right corner
yOffset += (editorHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
return {
zoomLevel,
offset: [closestNumberDivisibleBy(xOffset, GRID_SIZE), closestNumberDivisibleBy(yOffset, GRID_SIZE)],
};
};
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 = (nodeId: string, outputIndex: number) => {
return `${nodeId}${OUTPUT_UUID_KEY}${outputIndex}`;
};
export const getInputEndpointUUID = (nodeId: string, inputIndex: number) => {
return `${nodeId}${INPUT_UUID_KEY}${inputIndex}`;
};
export const getFixedNodesList = (workflowNodes: INode[]) => {
const nodes = [...workflowNodes];
const leftmostTop = getLeftmostTopNode(nodes);
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
nodes.map((node) => {
node.position[0] += diffX + (NODE_SIZE * 2);
node.position[1] += diffY;
});
return nodes;
}; };
export const getNodeViewTab = (route: Route): string|null => { export const getNodeViewTab = (route: Route): string|null => {

View file

@ -0,0 +1,725 @@
import { closestNumberDivisibleBy, getStyleTokenValue, isNumber } from "@/utils";
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from "@/constants";
import { EndpointStyle, IBounds, INodeUi, XYPosition } from "@/Interface";
import { AnchorArraySpec, Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
import {
IConnection,
INode,
ITaskData,
INodeExecutionData,
NodeInputConnections,
INodeTypeDescription,
} from 'n8n-workflow';
/*
Canvas constants and functions.
These utils are not exported with main `utils`package because they need to be used
on-demand (when jsplumb instance is ready) by components (mainly the NodeView).
*/
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 PLACEHOLDER_TRIGGER_NODE_SIZE = 100;
export const DEFAULT_START_POSITION_X = 180;
export const DEFAULT_START_POSITION_Y = 240;
export const HEADER_HEIGHT = 65;
export const SIDEBAR_WIDTH = 65;
export const INNER_SIDEBAR_WIDTH = 310;
export const SIDEBAR_WIDTH_EXPANDED = 200;
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 PLACEHOLDER_BUTTON = 'PlaceholderTriggerButton';
export const DEFAULT_PLACEHOLDER_TRIGGER_BUTTON = {
name: 'Choose a Trigger...',
type: PLACEHOLDER_BUTTON,
typeVersion: 1,
position: [],
parameters: {
height: PLACEHOLDER_TRIGGER_NODE_SIZE,
width: PLACEHOLDER_TRIGGER_NODE_SIZE,
},
};
export const WELCOME_STICKY_NODE = {
name: QUICKSTART_NOTE_NAME,
type: STICKY_NODE_TYPE,
typeVersion: 1,
position: [
0,
0,
] as XYPosition,
parameters: {
height: 300,
width: 380,
},
};
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
cornerRadius: 12,
stub: JSPLUMB_FLOWCHART_STUB + 10,
targetGap: 4,
alwaysRespectStubs: false,
loopbackVerticalLength: NODE_SIZE + GRID_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 totalEndpoints = endpoint && endpoint.__meta ? endpoint.__meta.totalEndpoints : 0;
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
const outputsOffset = totalEndpoints > 3 ? 24 : 0; // avoid intersecting plus
return index * indexOffset + labelOffset + outputsOffset;
},
}];
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 ANCHOR_POSITIONS: {
[key: string]: {
[key: number]: AnchorArraySpec[];
}
} = {
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): EndpointStyle => ({
width: 8,
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
fill: getStyleTokenValue(color),
stroke: getStyleTokenValue(color),
lineWidth: 0,
});
export const getInputNameOverlay = (label: string): OverlaySpec => ([
'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): OverlaySpec => ([
'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) => {
const hasCustomDimensions = [STICKY_NODE_TYPE, PLACEHOLDER_BUTTON].includes(node.type);
const xOffset = hasCustomDimensions && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
const yOffset = hasCustomDimensions && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
const x = node.position[0];
const y = node.position[1];
if (x < accu.minX) {
accu.minX = x;
}
if (y < accu.minY) {
accu.minY = y;
}
if ((x + xOffset) > accu.maxX) {
accu.maxX = x + xOffset;
}
if ((y + yOffset) > accu.maxY) {
accu.maxY = y + yOffset;
}
return accu;
}, {
minX: nodes[0].position[0],
minY: nodes[0].position[1],
maxX: nodes[0].position[0],
maxY: nodes[0].position[1],
});
};
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] = closestNumberDivisibleBy(targetPosition[0], GRID_SIZE);
targetPosition[1] = closestNumberDivisibleBy(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 getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => {
const { editorWidth, editorHeight } = getContentDimensions();
return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
};
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition, executionPreview: boolean) => {
const squareSize = GRID_SIZE * scale;
const dotSize = 1 * scale;
const dotPosition = (GRID_SIZE / 2) * scale;
if (executionPreview) {
return {
'background-image': 'linear-gradient(135deg, #f9f9fb 25%, #ffffff 25%, #ffffff 50%, #f9f9fb 50%, #f9f9fb 75%, #ffffff 75%, #ffffff 100%)',
'background-size': `${squareSize}px ${squareSize}px`,
'background-position': `left ${offsetPosition[0]}px top ${offsetPosition[1]}px`,
};
}
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 showConnectionActions = (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) => {
const sourceOutputIndex = i;
if (!outputMap[sourceOutputIndex]) {
outputMap[sourceOutputIndex] = {};
}
if (!outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY]) {
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY] = {};
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0] = {
total: 0,
iterations: 0,
};
}
const defaultOutput = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
defaultOutput.total += output ? output.length : 0;
defaultOutput.iterations += output ? 1 : 0;
if (!nodeConnections[sourceOutputIndex]) {
return;
}
nodeConnections[sourceOutputIndex]
.map((connection: IConnection) => {
const targetNodeName = connection.node;
const targetInputIndex = connection.index;
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 getRunItemsLabel = (output: {total: number, iterations: number}): string => {
let label = `${output.total}`;
label = output.total > 1 ? `${label} items` : `${label} item`;
label = output.iterations > 1 ? `${label} total` : label;
return label;
};
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);
}
connection.addOverlay([
'Label',
{
id: OVERLAY_RUN_ITEMS_ID,
label: `<span>${getRunItemsLabel(output)}</span>`,
cssClass: 'connection-run-items-label',
location: .5,
},
]);
showOrHideItemsLabel(connection);
showOrHideMidpointArrow(connection);
};
const getContentDimensions = (): { editorWidth: number, editorHeight: number } => {
let contentWidth = window.innerWidth;
let contentHeight = window.innerHeight;
const nodeViewRoot = document.getElementById('node-view-root');
if (nodeViewRoot) {
const contentBounds = nodeViewRoot.getBoundingClientRect();
contentWidth = contentBounds.width;
contentHeight = contentBounds.height;
}
return {
editorWidth: contentWidth,
editorHeight: contentHeight,
};
};
export const getZoomToFit = (nodes: INodeUi[], addFooterPadding = true): {offset: XYPosition, zoomLevel: number} => {
const { minX, minY, maxX, maxY } = getWorkflowCorners(nodes);
const { editorWidth, editorHeight } = getContentDimensions();
const footerHeight = addFooterPadding ? 200 : 100;
const PADDING = NODE_SIZE * 4;
const diffX = maxX - minX + PADDING;
const scaleX = editorWidth / diffX;
const diffY = maxY - minY + PADDING;
const scaleY = editorHeight / diffY;
const zoomLevel = Math.min(scaleX, scaleY, 1);
let xOffset = (minX * -1) * zoomLevel; // find top right corner
xOffset += (editorWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
let yOffset = (minY * -1) * zoomLevel; // find top right corner
yOffset += (editorHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
return {
zoomLevel,
offset: [closestNumberDivisibleBy(xOffset, GRID_SIZE), closestNumberDivisibleBy(yOffset, GRID_SIZE)],
};
};
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 = (nodeId: string, outputIndex: number) => {
return `${nodeId}${OUTPUT_UUID_KEY}${outputIndex}`;
};
export const getInputEndpointUUID = (nodeId: string, inputIndex: number) => {
return `${nodeId}${INPUT_UUID_KEY}${inputIndex}`;
};
export const getFixedNodesList = (workflowNodes: INode[]) => {
const nodes = [...workflowNodes];
const leftmostTop = getLeftmostTopNode(nodes);
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
nodes.map((node) => {
node.position[0] += diffX + (NODE_SIZE * 2);
node.position[1] += diffY;
});
return nodes;
};

View file

@ -172,41 +172,6 @@ import NodeSettings from '@/components/NodeSettings.vue';
import Sticky from '@/components/Sticky.vue'; import Sticky from '@/components/Sticky.vue';
import CanvasAddButton from './CanvasAddButton.vue'; import CanvasAddButton from './CanvasAddButton.vue';
import {
CONNECTOR_ARROW_OVERLAYS,
CONNECTOR_FLOWCHART_TYPE,
CONNECTOR_PAINT_STYLE_DEFAULT,
CONNECTOR_PAINT_STYLE_PRIMARY,
GRID_SIZE,
MAX_X_TO_PUSH_DOWNSTREAM_NODES,
NODE_SIZE,
PUSH_NODES_OFFSET,
WELCOME_STICKY_NODE,
addConnectionActionsOverlay,
addConnectionOutputSuccess,
getAccountAge,
getBackgroundStyles,
getConnectorLengths,
getFixedNodesList,
getMousePosition,
getNewNodePosition,
getNodeViewTab,
getInputEndpointUUID,
getOutputEndpointUUID,
getOutputSummary,
getRunItemsLabel,
hideConnectionActions,
moveBackInputLabelPosition,
showDropConnectionState,
showOrHideMidpointArrow,
showOrHideItemsLabel,
showPullConnectionState,
resetConnection,
resetConnectionAfterPull,
resetInputLabelPosition,
showConnectionActions,
} from '@/utils';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { import {
@ -265,6 +230,8 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator';
import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus'; import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
import { useCanvasStore } from '@/stores/canvas'; import { useCanvasStore } from '@/stores/canvas';
import useWorkflowsEEStore from "@/stores/workflows.ee"; import useWorkflowsEEStore from "@/stores/workflows.ee";
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { getAccountAge, getNodeViewTab } from '@/utils';
interface AddNodeOptions { interface AddNodeOptions {
position?: XYPosition; position?: XYPosition;
@ -488,7 +455,7 @@ export default mixins(
}; };
}, },
backgroundStyle(): object { backgroundStyle(): object {
return getBackgroundStyles( return NodeViewUtils.getBackgroundStyles(
this.nodeViewScale, this.nodeViewScale,
this.uiStore.nodeViewOffsetPosition, this.uiStore.nodeViewOffsetPosition,
this.isExecutionPreview, this.isExecutionPreview,
@ -549,7 +516,7 @@ export default mixins(
}, },
data() { data() {
return { return {
GRID_SIZE, GRID_SIZE: NodeViewUtils.GRID_SIZE,
STICKY_NODE_TYPE, STICKY_NODE_TYPE,
createNodeActive: false, createNodeActive: false,
lastSelectedConnection: null as null | Connection, lastSelectedConnection: null as null | Connection,
@ -781,7 +748,7 @@ export default mixins(
throw new Error('Invalid workflow object'); throw new Error('Invalid workflow object');
} }
this.resetWorkspace(); this.resetWorkspace();
data.workflow.nodes = getFixedNodesList(data.workflow.nodes); data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes);
await this.addNodes(data.workflow.nodes as INodeUi[], data.workflow.connections); await this.addNodes(data.workflow.nodes as INodeUi[], data.workflow.connections);
@ -820,7 +787,7 @@ export default mixins(
return; return;
} }
data.workflow.nodes = getFixedNodesList(data.workflow.nodes) as INodeUi[]; data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes) as INodeUi[];
this.blankRedirect = true; this.blankRedirect = true;
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } }); this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
@ -1453,7 +1420,7 @@ export default mixins(
// Fix the node position as it could be totally offscreen // Fix the node position as it could be totally offscreen
// and the pasted nodes would so not be directly visible to // and the pasted nodes would so not be directly visible to
// the user // the user
this.updateNodePositions(workflowData, getNewNodePosition(this.nodes, this.lastClickPosition)); this.updateNodePositions(workflowData, NodeViewUtils.getNewNodePosition(this.nodes, this.lastClickPosition));
const data = await this.addNodesToWorkflow(workflowData); const data = await this.addNodesToWorkflow(workflowData);
@ -1522,8 +1489,8 @@ export default mixins(
this.addNode(nodeTypeName, { this.addNode(nodeTypeName, {
position: [ position: [
mousePosition[0] - NODE_SIZE / 2, mousePosition[0] - NodeViewUtils.NODE_SIZE / 2,
mousePosition[1] - NODE_SIZE / 2, mousePosition[1] - NodeViewUtils.NODE_SIZE / 2,
], ],
dragAndDrop: true, dragAndDrop: true,
}); });
@ -1664,21 +1631,21 @@ export default mixins(
const lastSelectedNode = this.lastSelectedNode; const lastSelectedNode = this.lastSelectedNode;
if (options.position) { if (options.position) {
newNodeData.position = getNewNodePosition(this.canvasStore.getNodesWithPlaceholderNode(), options.position); newNodeData.position = NodeViewUtils.getNewNodePosition(this.canvasStore.getNodesWithPlaceholderNode(), options.position);
} else if (lastSelectedNode) { } else if (lastSelectedNode) {
const lastSelectedConnection = this.lastSelectedConnection; const lastSelectedConnection = this.lastSelectedConnection;
if (lastSelectedConnection) { // set when injecting into a connection if (lastSelectedConnection) { // set when injecting into a connection
const [diffX] = getConnectorLengths(lastSelectedConnection); const [diffX] = NodeViewUtils.getConnectorLengths(lastSelectedConnection);
if (diffX <= MAX_X_TO_PUSH_DOWNSTREAM_NODES) { if (diffX <= NodeViewUtils.MAX_X_TO_PUSH_DOWNSTREAM_NODES) {
this.pushDownstreamNodes(lastSelectedNode.name, PUSH_NODES_OFFSET); this.pushDownstreamNodes(lastSelectedNode.name, NodeViewUtils.PUSH_NODES_OFFSET);
} }
} }
// set when pulling connections // set when pulling connections
if (this.newNodeInsertPosition) { if (this.newNodeInsertPosition) {
newNodeData.position = getNewNodePosition(this.nodes, [ newNodeData.position = NodeViewUtils.getNewNodePosition(this.nodes, [
this.newNodeInsertPosition[0] + GRID_SIZE, this.newNodeInsertPosition[0] + NodeViewUtils.GRID_SIZE,
this.newNodeInsertPosition[1] - NODE_SIZE / 2, this.newNodeInsertPosition[1] - NodeViewUtils.NODE_SIZE / 2,
]); ]);
this.newNodeInsertPosition = null; this.newNodeInsertPosition = null;
} else { } else {
@ -1696,9 +1663,9 @@ export default mixins(
// If a node is active then add the new node directly after the current one // If a node is active then add the new node directly after the current one
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60]; // newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData.position = getNewNodePosition( newNodeData.position = NodeViewUtils.getNewNodePosition(
this.nodes, this.nodes,
[lastSelectedNode.position[0] + PUSH_NODES_OFFSET, lastSelectedNode.position[1] + yOffset], [lastSelectedNode.position[0] + NodeViewUtils.PUSH_NODES_OFFSET, lastSelectedNode.position[1] + yOffset],
[100, 0], [100, 0],
); );
} }
@ -1710,7 +1677,7 @@ export default mixins(
// If no node is active find a free spot // If no node is active find a free spot
: this.lastClickPosition as XYPosition; : this.lastClickPosition as XYPosition;
newNodeData.position = getNewNodePosition(this.nodes, position); newNodeData.position = NodeViewUtils.getNewNodePosition(this.nodes, position);
} }
@ -1820,12 +1787,12 @@ export default mixins(
}, },
initNodeView() { initNodeView() {
this.instance.importDefaults({ this.instance.importDefaults({
Connector: CONNECTOR_FLOWCHART_TYPE, Connector: NodeViewUtils.CONNECTOR_FLOWCHART_TYPE,
Endpoint: ['Dot', { radius: 5 }], Endpoint: ['Dot', { radius: 5 }],
DragOptions: { cursor: 'pointer', zIndex: 5000 }, DragOptions: { cursor: 'pointer', zIndex: 5000 },
PaintStyle: CONNECTOR_PAINT_STYLE_DEFAULT, PaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_DEFAULT,
HoverPaintStyle: CONNECTOR_PAINT_STYLE_PRIMARY, HoverPaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_PRIMARY,
ConnectionOverlays: CONNECTOR_ARROW_OVERLAYS, ConnectionOverlays: NodeViewUtils.CONNECTOR_ARROW_OVERLAYS,
Container: '#node-view', Container: '#node-view',
}); });
@ -1921,7 +1888,7 @@ export default mixins(
}; };
} }
resetConnection(info.connection); NodeViewUtils.resetConnection(info.connection);
if (!this.isReadOnly) { if (!this.isReadOnly) {
let exitTimer: NodeJS.Timeout | undefined; let exitTimer: NodeJS.Timeout | undefined;
@ -1941,14 +1908,14 @@ export default mixins(
return; return;
} }
hideConnectionActions(activeConnection); NodeViewUtils.hideConnectionActions(activeConnection);
enterTimer = setTimeout(() => { enterTimer = setTimeout(() => {
enterTimer = undefined; enterTimer = undefined;
if (info.connection) { if (info.connection) {
activeConnection = info.connection; activeConnection = info.connection;
showConnectionActions(info.connection); NodeViewUtils.showConnectionActions(info.connection);
} }
}, 150); }, 150);
} catch (e) { } catch (e) {
@ -1975,7 +1942,7 @@ export default mixins(
exitTimer = undefined; exitTimer = undefined;
if (info.connection && activeConnection === info.connection) { if (info.connection && activeConnection === info.connection) {
hideConnectionActions(activeConnection); NodeViewUtils.hideConnectionActions(activeConnection);
activeConnection = null; activeConnection = null;
} }
}, 500); }, 500);
@ -1984,7 +1951,7 @@ export default mixins(
} }
}); });
addConnectionActionsOverlay(info.connection, NodeViewUtils.addConnectionActionsOverlay(info.connection,
() => { () => {
activeConnection = null; activeConnection = null;
this.__deleteJSPlumbConnection(info.connection); this.__deleteJSPlumbConnection(info.connection);
@ -2001,7 +1968,7 @@ export default mixins(
}); });
} }
moveBackInputLabelPosition(info.targetEndpoint); NodeViewUtils.moveBackInputLabelPosition(info.targetEndpoint);
this.workflowsStore.addConnection({ this.workflowsStore.addConnection({
connection: [ connection: [
@ -2029,7 +1996,7 @@ export default mixins(
// calls the "connection" event but not the "connectionDetached" one. So we listen // calls the "connection" event but not the "connectionDetached" one. So we listen
// additionally to the "connectionMoved" event and then only delete the existing connection. // additionally to the "connectionMoved" event and then only delete the existing connection.
resetInputLabelPosition(info.originalTargetEndpoint); NodeViewUtils.resetInputLabelPosition(info.originalTargetEndpoint);
// @ts-ignore // @ts-ignore
const sourceInfo = info.originalSourceEndpoint.getParameters(); const sourceInfo = info.originalSourceEndpoint.getParameters();
@ -2057,7 +2024,7 @@ export default mixins(
this.instance.bind('connectionDetached', (info) => { this.instance.bind('connectionDetached', (info) => {
try { try {
resetInputLabelPosition(info.targetEndpoint); NodeViewUtils.resetInputLabelPosition(info.targetEndpoint);
info.connection.removeOverlays(); info.connection.removeOverlays();
this.__removeConnectionByConnectionInfo(info, false); this.__removeConnectionByConnectionInfo(info, false);
@ -2080,7 +2047,7 @@ export default mixins(
this.pullConnActiveNodeName = null; this.pullConnActiveNodeName = null;
this.pullConnActive = true; this.pullConnActive = true;
this.newNodeInsertPosition = null; this.newNodeInsertPosition = null;
resetConnection(connection); NodeViewUtils.resetConnection(connection);
const nodes = [...document.querySelectorAll('.node-default')]; const nodes = [...document.querySelectorAll('.node-default')];
@ -2092,14 +2059,14 @@ export default mixins(
const element = document.querySelector('.jtk-endpoint.dropHover'); const element = document.querySelector('.jtk-endpoint.dropHover');
if (element) { if (element) {
// @ts-ignore // @ts-ignore
showDropConnectionState(connection, element._jsPlumb); NodeViewUtils.showDropConnectionState(connection, element._jsPlumb);
return; return;
} }
const inputMargin = 24; const inputMargin = 24;
const intersecting = nodes.find((element: Element) => { const intersecting = nodes.find((element: Element) => {
const { top, left, right, bottom } = element.getBoundingClientRect(); const { top, left, right, bottom } = element.getBoundingClientRect();
const [x, y] = getMousePosition(e); const [x, y] = NodeViewUtils.getMousePosition(e);
if (top <= y && bottom >= y && (left - inputMargin) <= x && right >= x) { if (top <= y && bottom >= y && (left - inputMargin) <= x && right >= x) {
const nodeName = (element as HTMLElement).dataset['name'] as string; const nodeName = (element as HTMLElement).dataset['name'] as string;
const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null; const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null;
@ -2111,7 +2078,7 @@ export default mixins(
if (endpointUUID) { if (endpointUUID) {
const endpoint = this.instance.getEndpoint(endpointUUID); const endpoint = this.instance.getEndpoint(endpointUUID);
showDropConnectionState(connection, endpoint); NodeViewUtils.showDropConnectionState(connection, endpoint);
return true; return true;
} }
@ -2123,7 +2090,7 @@ export default mixins(
}); });
if (!intersecting) { if (!intersecting) {
showPullConnectionState(connection); NodeViewUtils.showPullConnectionState(connection);
this.pullConnActiveNodeName = null; this.pullConnActiveNodeName = null;
} }
}; };
@ -2131,7 +2098,7 @@ export default mixins(
const onMouseUp = (e: MouseEvent | TouchEvent) => { const onMouseUp = (e: MouseEvent | TouchEvent) => {
this.pullConnActive = false; this.pullConnActive = false;
this.newNodeInsertPosition = this.getMousePositionWithinNodeView(e); this.newNodeInsertPosition = this.getMousePositionWithinNodeView(e);
resetConnectionAfterPull(connection); NodeViewUtils.resetConnectionAfterPull(connection);
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('mouseup', onMouseUp);
}; };
@ -2164,7 +2131,7 @@ export default mixins(
this.workflowsStore.activeWorkflowExecution = null; this.workflowsStore.activeWorkflowExecution = null;
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
this.canvasStore.setZoomLevel(1); this.canvasStore.setZoomLevel(1, 0);
this.canvasStore.zoomToFit(); this.canvasStore.zoomToFit();
}, },
tryToAddWelcomeSticky: once(async function(this: any) { tryToAddWelcomeSticky: once(async function(this: any) {
@ -2174,19 +2141,19 @@ export default mixins(
// Inject welcome sticky note and zoom to fit // Inject welcome sticky note and zoom to fit
if (newWorkflow?.onboardingFlowEnabled && !this.isReadOnly) { if (newWorkflow?.onboardingFlowEnabled && !this.isReadOnly) {
const collisionPadding = GRID_SIZE + NODE_SIZE; const collisionPadding = NodeViewUtils.GRID_SIZE + NodeViewUtils.NODE_SIZE;
// Position the welcome sticky left to the added trigger node // Position the welcome sticky left to the added trigger node
let position: XYPosition = [...(this.triggerNodes[0].position as XYPosition)]; let position: XYPosition = [...(this.triggerNodes[0].position as XYPosition)];
position[0] -= WELCOME_STICKY_NODE.parameters.width + (GRID_SIZE * 4); position[0] -= NodeViewUtils.WELCOME_STICKY_NODE.parameters.width + (NodeViewUtils.GRID_SIZE * 4);
position = getNewNodePosition(this.nodes, position, [collisionPadding, collisionPadding]); position = NodeViewUtils.getNewNodePosition(this.nodes, position, [collisionPadding, collisionPadding]);
await this.addNodes([{ await this.addNodes([{
id: uuid(), id: uuid(),
...WELCOME_STICKY_NODE, ...NodeViewUtils.WELCOME_STICKY_NODE,
parameters: { parameters: {
// Use parameters from the template but add translated content // Use parameters from the template but add translated content
...WELCOME_STICKY_NODE.parameters, ...NodeViewUtils.WELCOME_STICKY_NODE.parameters,
content: this.$locale.baseText('onboardingWorkflow.stickyContent'), content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
}, },
position, position,
@ -2285,7 +2252,7 @@ export default mixins(
return null; return null;
} }
return getOutputEndpointUUID(node.id, index); return NodeViewUtils.getOutputEndpointUUID(node.id, index);
}, },
getInputEndpointUUID(nodeName: string, index: number) { getInputEndpointUUID(nodeName: string, index: number) {
const node = this.workflowsStore.getNodeByName(nodeName); const node = this.workflowsStore.getNodeByName(nodeName);
@ -2293,7 +2260,7 @@ export default mixins(
return null; return null;
} }
return getInputEndpointUUID(node.id, index); return NodeViewUtils.getInputEndpointUUID(node.id, index);
}, },
__addConnection(connection: [IConnection, IConnection], addVisualConnection = false) { __addConnection(connection: [IConnection, IConnection], addVisualConnection = false) {
if (addVisualConnection) { if (addVisualConnection) {
@ -2408,7 +2375,7 @@ export default mixins(
type: newNodeData.type, type: newNodeData.type,
}); });
newNodeData.position = getNewNodePosition( newNodeData.position = NodeViewUtils.getNewNodePosition(
this.nodes, this.nodes,
[node.position[0], node.position[1] + 140], [node.position[0], node.position[1] + 140],
[0, 140], [0, 140],
@ -2460,8 +2427,8 @@ export default mixins(
const sourceId = sourceNode.id; const sourceId = sourceNode.id;
const targetId = targetNode.id; const targetId = targetNode.id;
const sourceEndpoint = getOutputEndpointUUID(sourceId, sourceOutputIndex); const sourceEndpoint = NodeViewUtils.getOutputEndpointUUID(sourceId, sourceOutputIndex);
const targetEndpoint = getInputEndpointUUID(targetId, targetInputIndex); const targetEndpoint = NodeViewUtils.getInputEndpointUUID(targetId, targetInputIndex);
// @ts-ignore // @ts-ignore
const connections = this.instance.getConnections({ const connections = this.instance.getConnections({
@ -2508,8 +2475,8 @@ export default mixins(
const { incoming, outgoing } = this.getIncomingOutgoingConnections(node.name); const { incoming, outgoing } = this.getIncomingOutgoingConnections(node.name);
[...incoming, ...outgoing].forEach((connection: Connection) => { [...incoming, ...outgoing].forEach((connection: Connection) => {
showOrHideMidpointArrow(connection); NodeViewUtils.showOrHideMidpointArrow(connection);
showOrHideItemsLabel(connection); NodeViewUtils.showOrHideItemsLabel(connection);
}); });
}, },
onNodeRun({ name, data, waiting }: { name: string, data: ITaskData[] | null, waiting: boolean }) { onNodeRun({ name, data, waiting }: { name: string, data: ITaskData[] | null, waiting: boolean }) {
@ -2528,7 +2495,7 @@ export default mixins(
}) as Connection[]; }) as Connection[];
outgoing.forEach((connection: Connection) => { outgoing.forEach((connection: Connection) => {
resetConnection(connection); NodeViewUtils.resetConnection(connection);
}); });
const endpoints = this.getJSPlumbEndpoints(sourceNodeName); const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
endpoints.forEach((endpoint: Endpoint) => { endpoints.forEach((endpoint: Endpoint) => {
@ -2542,7 +2509,7 @@ export default mixins(
} }
const nodeConnections = this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main; const nodeConnections = this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main;
const outputMap = getOutputSummary(data, nodeConnections || []); const outputMap = NodeViewUtils.getOutputSummary(data, nodeConnections || []);
Object.keys(outputMap).forEach((sourceOutputIndex: string) => { Object.keys(outputMap).forEach((sourceOutputIndex: string) => {
Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => { Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => {
@ -2554,10 +2521,10 @@ export default mixins(
const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]; const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex];
if (!output || !output.total) { if (!output || !output.total) {
resetConnection(connection); NodeViewUtils.resetConnection(connection);
} }
else { else {
addConnectionOutputSuccess(connection, output); NodeViewUtils.addConnectionOutputSuccess(connection, output);
} }
} }
} }
@ -2566,7 +2533,7 @@ export default mixins(
if (endpoint && endpoint.endpoint) { if (endpoint && endpoint.endpoint) {
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0]; const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
if (output && output.total > 0) { if (output && output.total > 0) {
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(getRunItemsLabel(output)); (endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(NodeViewUtils.getRunItemsLabel(output));
} }
else { else {
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput(); (endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
@ -3237,7 +3204,7 @@ export default mixins(
}) as Connection[]; }) as Connection[];
connections.forEach((connection) => { connections.forEach((connection) => {
addConnectionOutputSuccess(connection, { NodeViewUtils.addConnectionOutputSuccess(connection, {
total: pinData[nodeName].length, total: pinData[nodeName].length,
iterations: 0, iterations: 0,
}); });
@ -3256,7 +3223,7 @@ export default mixins(
source: node.id, source: node.id,
}) as Connection[]; }) as Connection[];
connections.forEach(resetConnection); connections.forEach(NodeViewUtils.resetConnection);
}); });
}, },
onToggleNodeCreator({ source, createNodeActive }: { source?: string; createNodeActive: boolean }) { onToggleNodeCreator({ source, createNodeActive }: { source?: string; createNodeActive: boolean }) {