2023-11-28 03:15:08 -08:00
|
|
|
import { isNumber } from '@/utils/typeGuards';
|
2023-04-25 02:47:03 -07:00
|
|
|
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE } from '@/constants';
|
2023-04-24 03:18:24 -07:00
|
|
|
import type { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface';
|
|
|
|
import type { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common';
|
|
|
|
import type { Endpoint, Connection } from '@jsplumb/core';
|
2023-01-30 09:20:50 -08:00
|
|
|
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
|
2023-04-24 03:18:24 -07:00
|
|
|
import type {
|
2023-10-02 08:33:43 -07:00
|
|
|
ConnectionTypes,
|
2022-11-24 01:52:56 -08:00
|
|
|
IConnection,
|
|
|
|
ITaskData,
|
|
|
|
INodeExecutionData,
|
|
|
|
NodeInputConnections,
|
|
|
|
INodeTypeDescription,
|
|
|
|
} from 'n8n-workflow';
|
2023-10-02 08:33:43 -07:00
|
|
|
import { NodeConnectionType } from 'n8n-workflow';
|
2023-01-30 09:20:50 -08:00
|
|
|
import { EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER } from '@jsplumb/browser-ui';
|
2023-11-28 03:15:08 -08:00
|
|
|
import { useUIStore } from '@/stores/ui.store';
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
/*
|
|
|
|
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';
|
2023-01-30 09:20:50 -08:00
|
|
|
export const OVERLAY_INPUT_NAME_MOVED_CLASS = 'node-input-endpoint-label--moved';
|
2022-11-24 01:52:56 -08:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
export const CONNECTOR_FLOWCHART_TYPE: ConnectorSpec = {
|
|
|
|
type: N8nConnector.type,
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
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
|
2023-10-02 08:33:43 -07:00
|
|
|
const index = endpoint?.__meta ? endpoint.__meta.index : 0;
|
|
|
|
const totalEndpoints = endpoint?.__meta ? endpoint.__meta.totalEndpoints : 0;
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
2023-10-02 08:33:43 -07:00
|
|
|
const labelOffset = outputOverlay?.label && outputOverlay.label.length > 1 ? 10 : 0;
|
2022-11-24 01:52:56 -08:00
|
|
|
const outputsOffset = totalEndpoints > 3 ? 24 : 0; // avoid intersecting plus
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
return index * indexOffset + labelOffset + outputsOffset;
|
2022-12-14 01:04:10 -08:00
|
|
|
},
|
2022-11-24 01:52:56 -08:00
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
};
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
2023-11-01 09:56:15 -07:00
|
|
|
stroke: 'var(--color-foreground-dark)',
|
2022-11-24 01:52:56 -08:00
|
|
|
strokeWidth: 2,
|
|
|
|
outlineWidth: 12,
|
|
|
|
outlineStroke: 'transparent',
|
|
|
|
};
|
|
|
|
|
|
|
|
export const CONNECTOR_PAINT_STYLE_PULL: PaintStyle = {
|
|
|
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
2023-11-01 09:56:15 -07:00
|
|
|
stroke: 'var(--color-foreground-xdark)',
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const CONNECTOR_PAINT_STYLE_PRIMARY = {
|
|
|
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
2023-11-01 09:56:15 -07:00
|
|
|
stroke: 'var(--color-primary)',
|
2023-10-02 08:33:43 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
export const CONNECTOR_PAINT_STYLE_DATA: PaintStyle = {
|
|
|
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
|
|
|
...{
|
|
|
|
dashstyle: '5 3',
|
|
|
|
},
|
2023-11-01 09:56:15 -07:00
|
|
|
stroke: 'var(--color-foreground-dark)',
|
2023-10-02 08:33:43 -07:00
|
|
|
};
|
|
|
|
|
2023-10-30 10:42:47 -07:00
|
|
|
export const getConnectorColor = (type: ConnectionTypes, category?: string): string => {
|
|
|
|
if (category === 'error') {
|
|
|
|
return '--node-error-output-color';
|
|
|
|
}
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
if (type === NodeConnectionType.Main) {
|
|
|
|
return '--node-type-main-color';
|
|
|
|
}
|
|
|
|
|
|
|
|
return '--node-type-supplemental-connector-color';
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getConnectorPaintStylePull = (connection: Connection): PaintStyle => {
|
2023-10-30 10:42:47 -07:00
|
|
|
const connectorColor = getConnectorColor(
|
|
|
|
connection.parameters.type as ConnectionTypes,
|
|
|
|
connection.parameters.category,
|
|
|
|
);
|
2023-10-02 08:33:43 -07:00
|
|
|
const additionalStyles: PaintStyle = {};
|
|
|
|
if (connection.parameters.type !== NodeConnectionType.Main) {
|
|
|
|
additionalStyles.dashstyle = '5 3';
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...CONNECTOR_PAINT_STYLE_PULL,
|
2023-11-01 09:56:15 -07:00
|
|
|
...(connectorColor ? { stroke: `var(${connectorColor})` } : {}),
|
2023-10-02 08:33:43 -07:00
|
|
|
...additionalStyles,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getConnectorPaintStyleDefault = (connection: Connection): PaintStyle => {
|
2023-10-30 10:42:47 -07:00
|
|
|
const connectorColor = getConnectorColor(
|
|
|
|
connection.parameters.type as ConnectionTypes,
|
|
|
|
connection.parameters.category,
|
|
|
|
);
|
2023-10-02 08:33:43 -07:00
|
|
|
return {
|
|
|
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
2023-11-01 09:56:15 -07:00
|
|
|
...(connectorColor ? { stroke: `var(${connectorColor})` } : {}),
|
2023-10-02 08:33:43 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-10-30 10:42:47 -07:00
|
|
|
export const getConnectorPaintStyleData = (
|
|
|
|
connection: Connection,
|
|
|
|
category?: string,
|
|
|
|
): PaintStyle => {
|
|
|
|
const connectorColor = getConnectorColor(connection.parameters.type as ConnectionTypes, category);
|
2023-10-02 08:33:43 -07:00
|
|
|
return {
|
|
|
|
...CONNECTOR_PAINT_STYLE_DATA,
|
2023-11-01 09:56:15 -07:00
|
|
|
...(connectorColor ? { stroke: `var(${connectorColor})` } : {}),
|
2023-10-02 08:33:43 -07:00
|
|
|
};
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
2023-01-30 09:20:50 -08:00
|
|
|
{
|
|
|
|
type: 'Arrow',
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
id: OVERLAY_ENDPOINT_ARROW_ID,
|
|
|
|
location: 1,
|
|
|
|
width: 12,
|
|
|
|
foldback: 1,
|
|
|
|
length: 10,
|
|
|
|
visible: true,
|
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'Arrow',
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
id: OVERLAY_MIDPOINT_ARROW_ID,
|
|
|
|
location: 0.5,
|
|
|
|
width: 12,
|
|
|
|
foldback: 1,
|
|
|
|
length: 10,
|
|
|
|
visible: false,
|
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
},
|
2022-11-24 01:52:56 -08:00
|
|
|
];
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
export const getAnchorPosition = (
|
|
|
|
connectionType: ConnectionTypes,
|
|
|
|
type: 'input' | 'output',
|
|
|
|
amount: number,
|
|
|
|
spacerIndexes: number[] = [],
|
|
|
|
): ArrayAnchorSpec[] => {
|
|
|
|
if (connectionType === NodeConnectionType.Main) {
|
2023-10-25 05:34:47 -07:00
|
|
|
const anchors: ArrayAnchorSpec[] = [];
|
|
|
|
const x = type === 'input' ? 0.01 : 0.99;
|
|
|
|
const ox = type === 'input' ? -1 : 1;
|
|
|
|
const oy = 0;
|
|
|
|
const stepSize = 1 / (amount + 1); // +1 to not touch the node boundaries
|
|
|
|
|
|
|
|
for (let i = 1; i <= amount; i++) {
|
|
|
|
const y = stepSize * i; // Multiply by index to set position
|
|
|
|
anchors.push([x, y, ox, oy]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return anchors;
|
2023-10-02 08:33:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const y = type === 'input' ? 0.99 : 0.01;
|
|
|
|
const oy = type === 'input' ? 1 : -1;
|
|
|
|
const ox = 0;
|
|
|
|
|
|
|
|
const spacedAmount = amount + spacerIndexes.length;
|
|
|
|
const returnPositions: ArrayAnchorSpec[] = [];
|
|
|
|
for (let i = 0; i < spacedAmount; i++) {
|
|
|
|
const stepSize = 1 / (spacedAmount + 1);
|
|
|
|
let x = stepSize * i;
|
|
|
|
x += stepSize;
|
|
|
|
|
|
|
|
if (spacerIndexes.includes(i)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
returnPositions.push([x, y, ox, oy]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnPositions;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getScope = (type?: string) => {
|
|
|
|
if (!type || type === NodeConnectionType.Main) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return type;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getEndpointScope = (endpointType: ConnectionTypes): string | undefined => {
|
|
|
|
if (Object.values(NodeConnectionType).includes(endpointType)) {
|
|
|
|
return getScope(endpointType);
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const getInputEndpointStyle = (
|
|
|
|
nodeTypeData: INodeTypeDescription,
|
|
|
|
color: string,
|
2023-10-02 08:33:43 -07:00
|
|
|
connectionType: ConnectionTypes = NodeConnectionType.Main,
|
|
|
|
): EndpointStyle => {
|
|
|
|
let width = 8;
|
|
|
|
let height = nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20;
|
|
|
|
|
|
|
|
if (connectionType !== NodeConnectionType.Main) {
|
|
|
|
const temp = width;
|
|
|
|
width = height;
|
|
|
|
height = temp;
|
|
|
|
}
|
2022-11-24 01:52:56 -08:00
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
return {
|
|
|
|
width,
|
|
|
|
height,
|
2023-11-01 09:56:15 -07:00
|
|
|
fill: `var(${color})`,
|
|
|
|
stroke: `var(${color})`,
|
2023-10-02 08:33:43 -07:00
|
|
|
lineWidth: 0,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getInputNameOverlay = (
|
|
|
|
labelText: string,
|
|
|
|
inputName: string,
|
|
|
|
required?: boolean,
|
|
|
|
): OverlaySpec => ({
|
2023-01-30 09:20:50 -08:00
|
|
|
type: 'Custom',
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
id: OVERLAY_INPUT_NAME_LABEL,
|
|
|
|
visible: true,
|
2023-10-02 08:33:43 -07:00
|
|
|
location: [-1, -1],
|
2023-01-30 09:20:50 -08:00
|
|
|
create: (component: Endpoint) => {
|
|
|
|
const label = document.createElement('div');
|
|
|
|
label.innerHTML = labelText;
|
2023-10-02 08:33:43 -07:00
|
|
|
if (required) {
|
|
|
|
label.innerHTML += ' <strong style="color: var(--color-primary)">*</strong>';
|
|
|
|
}
|
2023-01-30 09:20:50 -08:00
|
|
|
label.classList.add('node-input-endpoint-label');
|
2023-12-08 04:42:32 -08:00
|
|
|
label.classList.add(`node-connection-type-${inputName ?? 'main'}`);
|
2023-10-02 08:33:43 -07:00
|
|
|
if (inputName !== NodeConnectionType.Main) {
|
|
|
|
label.classList.add('node-input-endpoint-label--data');
|
|
|
|
}
|
2023-01-30 09:20:50 -08:00
|
|
|
return label;
|
|
|
|
},
|
2022-11-24 01:52:56 -08:00
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
});
|
2022-11-24 01:52:56 -08:00
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
export const getOutputEndpointStyle = (
|
|
|
|
nodeTypeData: INodeTypeDescription,
|
|
|
|
color: string,
|
|
|
|
): PaintStyle => ({
|
|
|
|
strokeWidth: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
2023-11-01 09:56:15 -07:00
|
|
|
fill: `var(${color})`,
|
2022-11-24 01:52:56 -08:00
|
|
|
outlineStroke: 'none',
|
|
|
|
});
|
|
|
|
|
2023-10-25 05:34:47 -07:00
|
|
|
export const getOutputNameOverlay = (
|
|
|
|
labelText: string,
|
|
|
|
outputName: ConnectionTypes,
|
2023-10-30 10:42:47 -07:00
|
|
|
category?: string,
|
2023-10-25 05:34:47 -07:00
|
|
|
): OverlaySpec => ({
|
2023-01-30 09:20:50 -08:00
|
|
|
type: 'Custom',
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
id: OVERLAY_OUTPUT_NAME_LABEL,
|
|
|
|
visible: true,
|
2023-10-25 05:34:47 -07:00
|
|
|
create: (ep: Endpoint) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
const label = document.createElement('div');
|
|
|
|
label.innerHTML = labelText;
|
|
|
|
label.classList.add('node-output-endpoint-label');
|
2023-10-25 05:34:47 -07:00
|
|
|
|
2023-10-30 10:42:47 -07:00
|
|
|
if (ep?.__meta?.endpointLabelLength) {
|
|
|
|
label.setAttribute('data-endpoint-label-length', ep?.__meta?.endpointLabelLength);
|
|
|
|
}
|
2023-12-08 04:42:32 -08:00
|
|
|
label.classList.add(`node-connection-type-${getScope(outputName) ?? 'main'}`);
|
2023-10-02 08:33:43 -07:00
|
|
|
if (outputName !== NodeConnectionType.Main) {
|
|
|
|
label.classList.add('node-output-endpoint-label--data');
|
|
|
|
}
|
2023-10-30 10:42:47 -07:00
|
|
|
if (category) {
|
|
|
|
label.classList.add(`node-connection-category-${category}`);
|
|
|
|
}
|
2023-01-30 09:20:50 -08:00
|
|
|
return label;
|
|
|
|
},
|
2022-11-24 01:52:56 -08:00
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
});
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
|
|
|
overlays.forEach((overlay: OverlaySpec) => {
|
|
|
|
connection.addOverlay(overlay);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-11-27 06:30:28 -08:00
|
|
|
export const getLeftmostTopNode = <T extends { position: XYPosition }>(nodes: T[]): T => {
|
2022-11-24 01:52:56 -08:00
|
|
|
return nodes.reduce((leftmostTop, node) => {
|
|
|
|
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
|
|
|
return leftmostTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
return node;
|
2023-10-26 11:47:42 -07:00
|
|
|
}, nodes[0]);
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
const x = node.position[0];
|
|
|
|
const y = node.position[1];
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
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) => {
|
2023-10-02 08:33:43 -07:00
|
|
|
if (!connection?.endpoints || connection.endpoints.length !== 2) {
|
2022-11-24 01:52:56 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
|
|
|
|
|
|
|
const sourceEndpoint = connection.endpoints[0];
|
|
|
|
const targetEndpoint = connection.endpoints[1];
|
2023-01-30 09:20:50 -08:00
|
|
|
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
|
|
|
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? sourcePosition + 1;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
const minimum = hasItemsLabel ? 150 : 0;
|
|
|
|
const isBackwards = sourcePosition >= targetPosition;
|
|
|
|
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
2023-01-30 09:20:50 -08:00
|
|
|
const isActionsOverlayHovered = getOverlay(
|
|
|
|
connection,
|
|
|
|
OVERLAY_CONNECTION_ACTIONS_ID,
|
|
|
|
)?.component.isHover();
|
|
|
|
const isConnectionHovered = connection.isHover();
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
2023-01-30 09:20:50 -08:00
|
|
|
const isArrowVisible =
|
2023-10-02 08:33:43 -07:00
|
|
|
connection.parameters.type === NodeConnectionType.Main &&
|
2023-01-30 09:20:50 -08:00
|
|
|
isBackwards &&
|
|
|
|
isTooLong &&
|
|
|
|
!isActionsOverlayHovered &&
|
|
|
|
!isConnectionHovered &&
|
|
|
|
!connection.instance.isConnectionBeingDragged;
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
if (arrow) {
|
2023-01-30 09:20:50 -08:00
|
|
|
arrow.setVisible(isArrowVisible);
|
2022-11-24 01:52:56 -08:00
|
|
|
arrow.setLocation(hasItemsLabel ? 0.6 : 0.5);
|
2023-01-30 09:20:50 -08:00
|
|
|
connection.instance.repaint(arrow.canvas);
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getConnectorLengths = (connection: Connection): [number, number] => {
|
|
|
|
if (!connection.connector) {
|
|
|
|
return [0, 0];
|
|
|
|
}
|
|
|
|
const bounds = connection.connector.bounds;
|
2023-01-30 09:20:50 -08:00
|
|
|
const diffX = Math.abs(bounds.xmax - bounds.xmin);
|
|
|
|
const diffY = Math.abs(bounds.ymax - bounds.ymin);
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
return [diffX, diffY];
|
|
|
|
};
|
|
|
|
|
|
|
|
const isLoopingBackwards = (connection: Connection) => {
|
|
|
|
const sourceEndpoint = connection.endpoints[0];
|
|
|
|
const targetEndpoint = connection.endpoints[1];
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
|
|
|
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? 0;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
return targetPosition - sourcePosition < -1 * LOOPBACK_MINIMUM;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const showOrHideItemsLabel = (connection: Connection) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
if (!connection?.connector) return;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
2023-01-30 09:20:50 -08:00
|
|
|
if (!overlay) return;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
2023-01-30 09:20:50 -08:00
|
|
|
const isActionsOverlayHovered = actionsOverlay?.component.isHover();
|
|
|
|
|
|
|
|
if (isActionsOverlayHovered) {
|
2022-11-24 01:52:56 -08:00
|
|
|
overlay.setVisible(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [diffX, diffY] = getConnectorLengths(connection);
|
2023-01-30 09:20:50 -08:00
|
|
|
const isHidden = diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
overlay.setVisible(!isHidden);
|
2023-10-02 08:33:43 -07:00
|
|
|
const innerElement = overlay.canvas?.querySelector('span');
|
2022-11-24 01:52:56 -08:00
|
|
|
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') {
|
2022-12-29 03:20:43 -08:00
|
|
|
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>';
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (name === 'plus') {
|
2022-12-29 03:20:43 -08:00
|
|
|
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>';
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2023-11-28 03:15:08 -08:00
|
|
|
const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => {
|
|
|
|
const quotient = Math.ceil(inputNumber / divisibleBy);
|
|
|
|
|
|
|
|
// 1st possible closest number
|
|
|
|
const inputNumber1 = divisibleBy * quotient;
|
|
|
|
|
|
|
|
// 2nd possible closest number
|
|
|
|
const inputNumber2 =
|
|
|
|
inputNumber * divisibleBy > 0 ? divisibleBy * (quotient + 1) : divisibleBy * (quotient - 1);
|
|
|
|
|
|
|
|
// if true, then inputNumber1 is the required closest number
|
|
|
|
if (Math.abs(inputNumber - inputNumber1) < Math.abs(inputNumber - inputNumber2)) {
|
|
|
|
return inputNumber1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// else inputNumber2 is the required closest number
|
|
|
|
return inputNumber2;
|
|
|
|
};
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
if (conflictFound) {
|
2022-11-24 01:52:56 -08:00
|
|
|
targetPosition[0] += movePosition[0];
|
|
|
|
targetPosition[1] += movePosition[1];
|
|
|
|
}
|
2023-10-02 08:33:43 -07:00
|
|
|
} while (conflictFound);
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
return targetPosition;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
|
|
|
|
// @ts-ignore
|
2023-10-02 08:33:43 -07:00
|
|
|
const x = e.pageX !== undefined ? e.pageX : e.touches?.[0]?.pageX ? e.touches[0].pageX : 0;
|
2022-11-24 01:52:56 -08:00
|
|
|
// @ts-ignore
|
2023-10-02 08:33:43 -07:00
|
|
|
const y = e.pageY !== undefined ? e.pageY : e.touches?.[0]?.pageY ? e.touches[0].pageY : 0;
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
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':
|
2023-11-01 05:33:36 -07:00
|
|
|
'linear-gradient(135deg, var(--color-canvas-read-only-line) 25%, var(--color-canvas-background) 25%, var(--color-canvas-background) 50%, var(--color-canvas-read-only-line) 50%, var(--color-canvas-read-only-line) 75%, var(--color-canvas-background) 75%, var(--color-canvas-background) 100%)',
|
2022-11-24 01:52:56 -08:00
|
|
|
'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) {
|
|
|
|
return {
|
|
|
|
...styles,
|
2023-11-01 09:56:15 -07:00
|
|
|
'background-image': `radial-gradient(circle at ${dotPosition}px ${dotPosition}px, var(--color-canvas-dot) ${dotSize}px, transparent 0)`,
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return styles;
|
|
|
|
};
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
export const hideConnectionActions = (connection: Connection) => {
|
|
|
|
connection.instance.setSuspendDrawing(true);
|
|
|
|
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
|
|
|
showOrHideMidpointArrow(connection);
|
|
|
|
showOrHideItemsLabel(connection);
|
|
|
|
connection.instance.setSuspendDrawing(false);
|
|
|
|
(connection.endpoints || []).forEach((endpoint) => {
|
|
|
|
connection.instance.repaint(endpoint.element);
|
|
|
|
});
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
export const showConnectionActions = (connection: Connection) => {
|
|
|
|
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);
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
2023-01-30 09:20:50 -08:00
|
|
|
|
|
|
|
(connection.endpoints || []).forEach((endpoint) => {
|
|
|
|
connection.instance.repaint(endpoint.element);
|
|
|
|
});
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
export const getOutputSummary = (
|
|
|
|
data: ITaskData[],
|
|
|
|
nodeConnections: NodeInputConnections,
|
|
|
|
connectionType: ConnectionTypes,
|
|
|
|
) => {
|
2022-11-24 01:52:56 -08:00
|
|
|
const outputMap: {
|
|
|
|
[sourceOutputIndex: string]: {
|
|
|
|
[targetNodeName: string]: {
|
2023-02-17 01:54:07 -08:00
|
|
|
[targetInputIndex: string]: {
|
|
|
|
total: number;
|
|
|
|
iterations: number;
|
2023-04-03 06:04:59 -07:00
|
|
|
isArtificialRecoveredEventItem?: boolean;
|
2023-02-17 01:54:07 -08:00
|
|
|
};
|
2022-12-14 01:04:10 -08:00
|
|
|
};
|
|
|
|
};
|
2022-11-24 01:52:56 -08:00
|
|
|
} = {};
|
|
|
|
|
|
|
|
data.forEach((run: ITaskData) => {
|
2023-11-28 07:47:28 -08:00
|
|
|
if (!run?.data?.[connectionType]) {
|
2022-11-24 01:52:56 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
run.data[connectionType].forEach((output: INodeExecutionData[] | null, i: number) => {
|
2022-11-24 01:52:56 -08:00
|
|
|
const sourceOutputIndex = i;
|
|
|
|
|
2023-04-03 06:04:59 -07:00
|
|
|
// executionData that was recovered by recoverEvents in the CLI will have an isArtificialRecoveredEventItem property
|
2023-02-17 01:54:07 -08:00
|
|
|
// to indicate that it was not part of the original executionData
|
|
|
|
// we do not want to count these items in the summary
|
2023-04-03 06:04:59 -07:00
|
|
|
// if (output?.[0]?.json?.isArtificialRecoveredEventItem) {
|
2023-02-17 01:54:07 -08:00
|
|
|
// return outputMap;
|
|
|
|
// }
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
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;
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
if (!outputMap[sourceOutputIndex][targetNodeName]) {
|
|
|
|
outputMap[sourceOutputIndex][targetNodeName] = {};
|
|
|
|
}
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
if (!outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]) {
|
|
|
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex] = {
|
|
|
|
total: 0,
|
|
|
|
iterations: 0,
|
|
|
|
};
|
|
|
|
}
|
2022-12-14 01:04:10 -08:00
|
|
|
|
2023-04-03 06:04:59 -07:00
|
|
|
if (output?.[0]?.json?.isArtificialRecoveredEventItem) {
|
2023-02-17 01:54:07 -08:00
|
|
|
outputMap[sourceOutputIndex][targetNodeName][
|
|
|
|
targetInputIndex
|
2023-04-03 06:04:59 -07:00
|
|
|
].isArtificialRecoveredEventItem = true;
|
2023-02-17 01:54:07 -08:00
|
|
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].total = 0;
|
|
|
|
} else {
|
|
|
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].total += output
|
|
|
|
? output.length
|
|
|
|
: 0;
|
|
|
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].iterations += output
|
|
|
|
? 1
|
|
|
|
: 0;
|
|
|
|
}
|
2022-11-24 01:52:56 -08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return outputMap;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const resetConnection = (connection: Connection) => {
|
|
|
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
2023-01-30 09:20:50 -08:00
|
|
|
connection.removeClass('success');
|
2022-11-24 01:52:56 -08:00
|
|
|
showOrHideMidpointArrow(connection);
|
2023-10-02 08:33:43 -07:00
|
|
|
connection.setPaintStyle(getConnectorPaintStyleDefault(connection));
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-02-17 01:54:07 -08:00
|
|
|
export const recoveredConnection = (connection: Connection) => {
|
|
|
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
|
|
|
connection.addClass('success');
|
|
|
|
showOrHideMidpointArrow(connection);
|
|
|
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
|
|
|
};
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
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 },
|
|
|
|
) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
connection.addClass('success');
|
2022-11-24 01:52:56 -08:00
|
|
|
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
|
|
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
|
|
|
}
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
if (connection.parameters.type === NodeConnectionType.Main) {
|
|
|
|
const overlay = connection.addOverlay({
|
|
|
|
type: 'Custom',
|
|
|
|
options: {
|
|
|
|
id: OVERLAY_RUN_ITEMS_ID,
|
|
|
|
create() {
|
|
|
|
const container = document.createElement('div');
|
|
|
|
const span = document.createElement('span');
|
|
|
|
|
|
|
|
container.classList.add('connection-run-items-label');
|
|
|
|
span.classList.add('floating');
|
|
|
|
span.innerHTML = getRunItemsLabel(output);
|
|
|
|
container.appendChild(span);
|
|
|
|
return container;
|
|
|
|
},
|
|
|
|
location: 0.5,
|
2023-01-30 09:20:50 -08:00
|
|
|
},
|
2023-10-02 08:33:43 -07:00
|
|
|
});
|
|
|
|
overlay.setVisible(true);
|
|
|
|
}
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
showOrHideItemsLabel(connection);
|
|
|
|
showOrHideMidpointArrow(connection);
|
2023-01-30 09:20:50 -08:00
|
|
|
|
|
|
|
(connection.endpoints || []).forEach((endpoint) => {
|
|
|
|
connection.instance.repaint(endpoint.element);
|
|
|
|
});
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2023-07-14 06:36:17 -07:00
|
|
|
const uiStore = useUIStore();
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
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
|
2023-07-14 06:36:17 -07:00
|
|
|
yOffset +=
|
|
|
|
(editorHeight -
|
|
|
|
(maxY - minY + footerHeight - uiStore.headerHeight + uiStore.bannersHeight) * zoomLevel) /
|
|
|
|
2; // add padding to center workflow
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
return {
|
|
|
|
zoomLevel,
|
|
|
|
offset: [
|
|
|
|
closestNumberDivisibleBy(xOffset, GRID_SIZE),
|
|
|
|
closestNumberDivisibleBy(yOffset, GRID_SIZE),
|
|
|
|
],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
if (connection?.connector) {
|
|
|
|
const connector = connection.connector as N8nConnector;
|
2022-11-24 01:52:56 -08:00
|
|
|
if (targetEndpoint) {
|
2023-01-30 09:20:50 -08:00
|
|
|
connector.setTargetEndpoint(targetEndpoint);
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
|
|
|
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const showPullConnectionState = (connection: Connection) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
if (connection?.connector) {
|
|
|
|
const connector = connection.connector as N8nConnector;
|
|
|
|
connector.resetTargetEndpoint();
|
2023-10-02 08:33:43 -07:00
|
|
|
connection.setPaintStyle(getConnectorPaintStylePull(connection));
|
2022-11-24 01:52:56 -08:00
|
|
|
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const resetConnectionAfterPull = (connection: Connection) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
if (connection?.connector) {
|
|
|
|
const connector = connection.connector as N8nConnector;
|
|
|
|
connector.resetTargetEndpoint();
|
2023-10-02 08:33:43 -07:00
|
|
|
connection.setPaintStyle(getConnectorPaintStyleDefault(connection));
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
export const resetInputLabelPosition = (targetEndpoint: Connection | Endpoint) => {
|
2022-11-24 01:52:56 -08:00
|
|
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
|
|
|
if (inputNameOverlay) {
|
2023-01-30 09:20:50 -08:00
|
|
|
targetEndpoint.instance.removeOverlayClass(inputNameOverlay, OVERLAY_INPUT_NAME_MOVED_CLASS);
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
export const hideOutputNameLabel = (sourceEndpoint: Connection | Endpoint) => {
|
|
|
|
hideOverlay(sourceEndpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const showOutputNameLabel = (
|
|
|
|
sourceEndpoint: Connection | Endpoint,
|
|
|
|
connection: Connection,
|
|
|
|
) => {
|
|
|
|
const outputNameOverlay = getOverlay(sourceEndpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
|
|
|
if (outputNameOverlay) {
|
|
|
|
outputNameOverlay.setVisible(true);
|
|
|
|
(connection.endpoints || []).forEach((endpoint) => {
|
|
|
|
connection.instance.repaint(endpoint.element);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
|
|
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
|
|
|
if (inputNameOverlay) {
|
2023-01-30 09:20:50 -08:00
|
|
|
targetEndpoint.instance.addOverlayClass(inputNameOverlay, OVERLAY_INPUT_NAME_MOVED_CLASS);
|
2022-11-24 01:52:56 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-02-07 07:34:08 -08:00
|
|
|
export const addConnectionTestData = (
|
|
|
|
source: HTMLElement,
|
|
|
|
target: HTMLElement,
|
|
|
|
el: HTMLElement | undefined,
|
|
|
|
) => {
|
|
|
|
// TODO: Only do this if running in test mode
|
|
|
|
const sourceNodeName = source.getAttribute('data-name')?.toString();
|
|
|
|
const targetNodeName = target.getAttribute('data-name')?.toString();
|
|
|
|
|
|
|
|
if (el && sourceNodeName && targetNodeName) {
|
|
|
|
el.setAttribute('data-source-node', sourceNodeName);
|
|
|
|
el.setAttribute('data-target-node', targetNodeName);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-11-24 01:52:56 -08:00
|
|
|
export const addConnectionActionsOverlay = (
|
|
|
|
connection: Connection,
|
|
|
|
onDelete: Function,
|
|
|
|
onAdd: Function,
|
|
|
|
) => {
|
2023-01-30 09:20:50 -08:00
|
|
|
const overlay = connection.addOverlay({
|
|
|
|
type: 'Custom',
|
|
|
|
options: {
|
2022-11-24 01:52:56 -08:00
|
|
|
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
2023-01-30 09:20:50 -08:00
|
|
|
create: (component: Connection) => {
|
|
|
|
const div = document.createElement('div');
|
|
|
|
const deleteButton = document.createElement('button');
|
|
|
|
|
|
|
|
div.classList.add(OVERLAY_CONNECTION_ACTIONS_ID);
|
2023-02-07 07:34:08 -08:00
|
|
|
addConnectionTestData(component.source, component.target, div);
|
2023-10-02 08:33:43 -07:00
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
deleteButton.innerHTML = getIcon('trash');
|
|
|
|
deleteButton.addEventListener('click', () => onDelete());
|
|
|
|
// We have to manually trigger connection mouse events because the overlay
|
|
|
|
// is not part of the connection element
|
|
|
|
div.addEventListener('mouseout', () =>
|
|
|
|
connection.instance.fire(EVENT_CONNECTION_MOUSEOUT, component),
|
|
|
|
);
|
|
|
|
div.addEventListener('mouseover', () =>
|
|
|
|
connection.instance.fire(EVENT_CONNECTION_MOUSEOVER, component),
|
|
|
|
);
|
2023-10-02 08:33:43 -07:00
|
|
|
|
|
|
|
if (connection.parameters.type === NodeConnectionType.Main) {
|
|
|
|
const addButton = document.createElement('button');
|
|
|
|
addButton.classList.add('add');
|
|
|
|
addButton.innerHTML = getIcon('plus');
|
|
|
|
addButton.addEventListener('click', () => onAdd());
|
|
|
|
div.appendChild(addButton);
|
|
|
|
deleteButton.classList.add('delete');
|
|
|
|
} else {
|
|
|
|
deleteButton.classList.add('delete-single');
|
|
|
|
}
|
|
|
|
|
2023-01-30 09:20:50 -08:00
|
|
|
div.appendChild(deleteButton);
|
|
|
|
return div;
|
2022-11-24 01:52:56 -08:00
|
|
|
},
|
|
|
|
},
|
2023-01-30 09:20:50 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
overlay.setVisible(false);
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
export const getOutputEndpointUUID = (
|
|
|
|
nodeId: string,
|
|
|
|
connectionType: ConnectionTypes,
|
|
|
|
outputIndex: number,
|
|
|
|
) => {
|
|
|
|
return `${nodeId}${OUTPUT_UUID_KEY}${getScope(connectionType) || ''}${outputIndex}`;
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-10-02 08:33:43 -07:00
|
|
|
export const getInputEndpointUUID = (
|
|
|
|
nodeId: string,
|
|
|
|
connectionType: ConnectionTypes,
|
|
|
|
inputIndex: number,
|
|
|
|
) => {
|
|
|
|
return `${nodeId}${INPUT_UUID_KEY}${getScope(connectionType) || ''}${inputIndex}`;
|
2022-11-24 01:52:56 -08:00
|
|
|
};
|
|
|
|
|
2023-11-27 06:30:28 -08:00
|
|
|
export const getFixedNodesList = <T extends { position: XYPosition }>(workflowNodes: T[]): T[] => {
|
2022-11-24 01:52:56 -08:00
|
|
|
const nodes = [...workflowNodes];
|
|
|
|
|
2023-10-26 11:47:42 -07:00
|
|
|
if (nodes.length) {
|
|
|
|
const leftmostTop = getLeftmostTopNode(nodes);
|
2022-11-24 01:52:56 -08:00
|
|
|
|
2023-10-26 11:47:42 -07:00
|
|
|
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
|
|
|
|
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
|
2022-11-24 01:52:56 -08:00
|
|
|
|
2023-10-26 11:47:42 -07:00
|
|
|
nodes.forEach((node) => {
|
|
|
|
node.position[0] += diffX + NODE_SIZE * 2;
|
|
|
|
node.position[1] += diffY;
|
|
|
|
});
|
|
|
|
}
|
2022-11-24 01:52:56 -08:00
|
|
|
|
|
|
|
return nodes;
|
|
|
|
};
|
2023-12-08 04:42:32 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the intersecting distances of the mouse event coordinates with the given element's boundaries,
|
|
|
|
* adjusted by the specified offset.
|
|
|
|
*
|
|
|
|
* @param {Element} element - The DOM element to check against.
|
|
|
|
* @param {MouseEvent | TouchEvent} mouseEvent - The mouse or touch event with the coordinates.
|
|
|
|
* @param {number} offset - Offset to adjust the element's boundaries.
|
|
|
|
* @returns { {x: number | null, y: number | null} | null } Object containing intersecting distances along x and y axes or null if no intersection.
|
|
|
|
*/
|
|
|
|
export function calculateElementIntersection(
|
|
|
|
element: Element,
|
|
|
|
mouseEvent: MouseEvent | TouchEvent,
|
|
|
|
offset: number,
|
|
|
|
): { x: number | null; y: number | null } | null {
|
|
|
|
const { top, left, right, bottom } = element.getBoundingClientRect();
|
|
|
|
const [x, y] = getMousePosition(mouseEvent);
|
|
|
|
|
|
|
|
let intersectX: number | null = null;
|
|
|
|
let intersectY: number | null = null;
|
|
|
|
|
|
|
|
if (x >= left - offset && x <= right + offset) {
|
|
|
|
intersectX = Math.min(x - (left - offset), right + offset - x);
|
|
|
|
}
|
|
|
|
if (y >= top - offset && y <= bottom + offset) {
|
|
|
|
intersectY = Math.min(y - (top - offset), bottom + offset - y);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (intersectX === null && intersectY === null) return null;
|
|
|
|
|
|
|
|
return { x: intersectX, y: intersectY };
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the mouse event coordinates intersect with the given element's boundaries,
|
|
|
|
* adjusted by the specified offset.
|
|
|
|
*
|
|
|
|
* @param {Element} element - The DOM element to check against.
|
|
|
|
* @param {MouseEvent | TouchEvent} mouseEvent - The mouse or touch event with the coordinates.
|
|
|
|
* @param {number} offset - Offset to adjust the element's boundaries.
|
|
|
|
* @returns {boolean} True if the mouse coordinates intersect with the element.
|
|
|
|
*/
|
|
|
|
export function isElementIntersection(
|
|
|
|
element: Element,
|
|
|
|
mouseEvent: MouseEvent | TouchEvent,
|
|
|
|
offset: number,
|
|
|
|
): boolean {
|
|
|
|
const intersection = calculateElementIntersection(element, mouseEvent, offset);
|
|
|
|
|
|
|
|
if (intersection === null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isWithinVerticalBounds = intersection.y !== null;
|
|
|
|
const isWithinHorizontalBounds = intersection.x !== null;
|
|
|
|
|
|
|
|
return isWithinVerticalBounds && isWithinHorizontalBounds;
|
|
|
|
}
|