2024-05-21 21:54:55 -07:00
import { isNumber , isValidNodeConnectionType } from '@/utils/typeGuards' ;
2024-09-04 06:33:10 -07:00
import {
LIST_LIKE_NODE_OPERATIONS ,
NODE_OUTPUT_DEFAULT_KEY ,
SET_NODE_TYPE ,
SPLIT_IN_BATCHES_NODE_TYPE ,
STICKY_NODE_TYPE ,
} from '@/constants' ;
2024-06-10 06:23:06 -07:00
import type { EndpointStyle , IBounds , INodeUi , XYPosition } from '@/Interface' ;
2023-04-24 03:18:24 -07:00
import type { ArrayAnchorSpec , ConnectorSpec , OverlaySpec , PaintStyle } from '@jsplumb/common' ;
2024-05-21 21:54:55 -07:00
import type { Connection , Endpoint , SelectOptions } 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 {
2024-09-04 06:33:10 -07:00
AssignmentCollectionValue ,
2022-11-24 01:52:56 -08:00
IConnection ,
2024-09-04 06:33:10 -07:00
INode ,
2022-11-24 01:52:56 -08:00
INodeExecutionData ,
INodeTypeDescription ,
2024-05-21 21:54:55 -07:00
ITaskData ,
2024-09-04 06:33:10 -07:00
NodeHint ,
2024-05-21 21:54:55 -07:00
NodeInputConnections ,
2024-09-04 06:33:10 -07:00
Workflow ,
2022-11-24 01:52:56 -08:00
} from 'n8n-workflow' ;
2024-09-04 06:33:10 -07:00
import { NodeConnectionType , NodeHelpers } from 'n8n-workflow' ;
2024-01-19 05:44:54 -08:00
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui' ;
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' ;
2024-05-31 06:52:00 -07:00
import type { StyleValue } from 'vue' ;
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 ;
2024-09-03 05:11:44 -07:00
export const DEFAULT_NODE_SIZE = [ 100 , 100 ] ;
export const CONFIGURATION_NODE_SIZE = [ 80 , 80 ] ;
export const CONFIGURABLE_NODE_SIZE = [ 256 , 100 ] ;
2022-11-24 01:52:56 -08:00
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
2024-06-10 06:23:06 -07:00
getEndpointOffset ( endpoint : Endpoint ) {
2022-11-24 01:52:56 -08:00
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 ) ;
2024-05-21 21:54:55 -07:00
const outputOverlayLabel =
outputOverlay && 'label' in outputOverlay ? ` ${ outputOverlay ? . label } ` : '' ;
const labelOffset = outputOverlayLabel . 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
} ;
2024-05-21 21:54:55 -07:00
export function isCanvasAugmentedType < T > ( overlay : T ) : overlay is T & { canvas : HTMLElement } {
return typeof overlay === 'object' && overlay !== null && 'canvas' in overlay && ! ! overlay . canvas ;
}
2024-08-29 06:55:53 -07:00
export const getConnectorColor = ( type : NodeConnectionType , category? : string ) : string = > {
2023-10-30 10:42:47 -07:00
if ( category === 'error' ) {
2024-04-07 23:21:54 -07:00
return '--color-node-error-output-text-color' ;
2023-10-30 10:42:47 -07:00
}
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 (
2024-08-29 06:55:53 -07:00
connection . parameters . type as NodeConnectionType ,
2023-10-30 10:42:47 -07:00
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 (
2024-08-29 06:55:53 -07:00
connection . parameters . type as NodeConnectionType ,
2023-10-30 10:42:47 -07:00
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 = > {
2024-08-29 06:55:53 -07:00
const connectorColor = getConnectorColor (
connection . parameters . type as NodeConnectionType ,
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 = (
2024-08-29 06:55:53 -07:00
connectionType : NodeConnectionType ,
2023-10-02 08:33:43 -07:00
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 ;
} ;
2024-05-21 21:54:55 -07:00
export const getScope = ( type ? : NodeConnectionType ) : NodeConnectionType | undefined = > {
2023-10-02 08:33:43 -07:00
if ( ! type || type === NodeConnectionType . Main ) {
return undefined ;
}
2024-05-21 21:54:55 -07:00
2023-10-02 08:33:43 -07:00
return type ;
} ;
2024-05-21 21:54:55 -07:00
export const getEndpointScope = (
endpointType : NodeConnectionType | string ,
) : NodeConnectionType | undefined = > {
if ( isValidNodeConnectionType ( endpointType ) ) {
2023-10-02 08:33:43 -07:00
return getScope ( endpointType ) ;
}
return undefined ;
2022-11-24 01:52:56 -08:00
} ;
export const getInputEndpointStyle = (
nodeTypeData : INodeTypeDescription ,
color : string ,
2024-08-29 06:55:53 -07:00
connectionType : NodeConnectionType = NodeConnectionType . Main ,
2023-10-02 08:33:43 -07:00
) : 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 ] ,
2024-05-21 21:54:55 -07:00
create : ( _ : Endpoint ) = > {
2023-01-30 09:20:50 -08:00
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 ,
2024-05-21 21:54:55 -07:00
outputName : NodeConnectionType ,
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 ,
2024-06-10 06:23:06 -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 ) {
2024-05-21 21:54:55 -07:00
label . setAttribute ( 'data-endpoint-label-length' , ` ${ ep ? . __meta ? . endpointLabelLength } ` ) ;
2023-10-30 10:42:47 -07:00
}
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 ) ;
2024-05-21 21:54:55 -07:00
if ( isCanvasAugmentedType ( arrow ) ) {
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 ) ;
2024-05-21 21:54:55 -07:00
const innerElement = isCanvasAugmentedType ( overlay )
? overlay . canvas . querySelector ( 'span' )
: undefined ;
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 ,
2024-05-31 06:52:00 -07:00
) : StyleValue = > {
2022-11-24 01:52:56 -08:00
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 ` ,
} ;
}
2024-05-31 06:52:00 -07:00
const styles : StyleValue = {
2022-11-24 01:52:56 -08:00
'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 ,
2024-08-29 06:55:53 -07:00
connectionType : NodeConnectionType ,
2023-10-02 08:33:43 -07:00
) = > {
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 ,
2024-01-11 05:03:23 -08:00
output : { total : number ; iterations : number ; classNames? : string [ ] } ,
2022-11-24 01:52:56 -08:00
) = > {
2024-01-11 05:03:23 -08:00
const classNames : string [ ] = [ 'success' ] ;
if ( output . classNames ) {
classNames . push ( . . . output . classNames ) ;
}
connection . addClass ( classNames . join ( ' ' ) ) ;
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' ) ;
2024-01-11 05:03:23 -08:00
container . classList . add ( . . . [ 'connection-run-items-label' , . . . classNames ] ) ;
2023-10-02 08:33:43 -07:00
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
} ;
2024-01-11 05:03:23 -08:00
export const addClassesToOverlays = ( {
connection ,
overlayIds ,
classNames ,
includeConnector ,
} : {
connection : Connection ;
overlayIds : string [ ] ;
classNames : string [ ] ;
includeConnector? : boolean ;
} ) = > {
overlayIds . forEach ( ( overlayId ) = > {
const overlay = getOverlay ( connection , overlayId ) ;
2024-05-21 21:54:55 -07:00
if ( overlay && isCanvasAugmentedType ( overlay ) ) {
overlay . canvas ? . classList . add ( . . . classNames ) ;
}
if ( includeConnector && isCanvasAugmentedType ( connection . connector ) ) {
2024-01-11 05:03:23 -08:00
connection . connector . canvas ? . classList . add ( . . . classNames ) ;
}
} ) ;
} ;
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 ,
2024-05-21 21:54:55 -07:00
connectionType : NodeConnectionType ,
2023-10-02 08:33:43 -07:00
outputIndex : number ,
) = > {
2024-05-21 21:54:55 -07:00
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 ,
2024-05-21 21:54:55 -07:00
connectionType : NodeConnectionType ,
2023-10-02 08:33:43 -07:00
inputIndex : number ,
) = > {
2024-05-21 21:54:55 -07:00
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 ;
}
2024-01-19 05:44:54 -08:00
export const getJSPlumbEndpoints = (
node : INodeUi | null ,
instance : BrowserJsPlumbInstance ,
) : Endpoint [ ] = > {
2024-05-21 21:54:55 -07:00
if ( ! node ) return [ ] ;
2024-01-19 05:44:54 -08:00
2024-05-21 21:54:55 -07:00
const nodeEl = instance . getManagedElement ( node ? . id ) ;
return instance ? . getEndpoints ( nodeEl ) ;
2024-01-19 05:44:54 -08:00
} ;
export const getPlusEndpoint = (
node : INodeUi | null ,
outputIndex : number ,
instance : BrowserJsPlumbInstance ,
) : Endpoint | undefined = > {
const endpoints = getJSPlumbEndpoints ( node , instance ) ;
return endpoints . find (
2024-06-10 06:23:06 -07:00
( endpoint : Endpoint ) = >
2024-01-19 05:44:54 -08:00
endpoint . endpoint . type === 'N8nPlus' && endpoint ? . __meta ? . index === outputIndex ,
) ;
} ;
export const getJSPlumbConnection = (
sourceNode : INodeUi | null ,
sourceOutputIndex : number ,
targetNode : INodeUi | null ,
targetInputIndex : number ,
2024-05-21 21:54:55 -07:00
connectionType : NodeConnectionType ,
2024-01-19 05:44:54 -08:00
sourceNodeType : INodeTypeDescription | null ,
instance : BrowserJsPlumbInstance ,
) : Connection | undefined = > {
if ( ! sourceNode || ! targetNode ) {
return ;
}
const sourceId = sourceNode . id ;
const targetId = targetNode . id ;
const sourceEndpoint = getOutputEndpointUUID ( sourceId , connectionType , sourceOutputIndex ) ;
const targetEndpoint = getInputEndpointUUID ( targetId , connectionType , targetInputIndex ) ;
2024-05-21 21:54:55 -07:00
const sourceNodeOutput = sourceNodeType ? . outputs ? . [ sourceOutputIndex ] ? ? NodeConnectionType . Main ;
2024-01-19 05:44:54 -08:00
const sourceNodeOutputName =
2024-05-21 21:54:55 -07:00
typeof sourceNodeOutput === 'string'
? sourceNodeOutput
: 'name' in sourceNodeOutput
? ` ${ sourceNodeOutput . name } `
: '' ;
2024-01-19 05:44:54 -08:00
const scope = getEndpointScope ( sourceNodeOutputName ) ;
const connections = instance ? . getConnections ( {
scope ,
source : sourceId ,
target : targetId ,
2024-05-21 21:54:55 -07:00
} as SelectOptions < Element > ) ;
if ( ! Array . isArray ( connections ) ) {
return ;
}
2024-01-19 05:44:54 -08:00
return connections . find ( ( connection : Connection ) = > {
const uuids = connection . getUuids ( ) ;
return uuids [ 0 ] === sourceEndpoint && uuids [ 1 ] === targetEndpoint ;
} ) ;
} ;
2024-09-04 06:33:10 -07:00
export function getGenericHints ( {
workflowNode ,
node ,
nodeType ,
nodeOutputData ,
hasMultipleInputItems ,
workflow ,
hasNodeRun ,
} : {
workflowNode : INode ;
node : INodeUi ;
nodeType : INodeTypeDescription ;
nodeOutputData : INodeExecutionData [ ] ;
hasMultipleInputItems : boolean ;
workflow : Workflow ;
hasNodeRun : boolean ;
} ) {
const nodeHints : NodeHint [ ] = [ ] ;
// add limit reached hint
if ( hasNodeRun && workflowNode . parameters . limit ) {
if ( nodeOutputData . length === workflowNode . parameters . limit ) {
nodeHints . push ( {
message : ` Limit of ${ workflowNode . parameters . limit as number } items reached. There may be more items that aren't being returned. Tweak the 'Return All' or 'Limit' parameters to access more items. ` ,
location : 'outputPane' ,
whenToDisplay : 'afterExecution' ,
} ) ;
}
}
// add Execute Once hint
if (
hasMultipleInputItems &&
LIST_LIKE_NODE_OPERATIONS . includes ( ( workflowNode . parameters . operation as string ) || '' )
) {
const executeOnce = workflow . getNode ( node . name ) ? . executeOnce ;
if ( ! executeOnce ) {
nodeHints . push ( {
message :
2024-09-06 01:24:48 -07:00
'This node runs multiple times, once for each input item. Use ‘ Execute Once’ in the node settings if you want to run it only once.' ,
2024-09-04 06:33:10 -07:00
location : 'outputPane' ,
} ) ;
}
}
// add expression in field name hint for Set node
if ( node . type === SET_NODE_TYPE && node . parameters . mode === 'manual' ) {
const rawParameters = NodeHelpers . getNodeParameters (
nodeType . properties ,
node . parameters ,
true ,
false ,
node ,
undefined ,
false ,
) ;
const assignments =
( ( rawParameters ? . assignments as AssignmentCollectionValue ) || { } ) ? . assignments || [ ] ;
const expressionInFieldName : number [ ] = [ ] ;
for ( const [ index , assignment ] of assignments . entries ( ) ) {
if ( assignment . name . startsWith ( '=' ) ) {
expressionInFieldName . push ( index + 1 ) ;
}
}
if ( expressionInFieldName . length > 0 ) {
nodeHints . push ( {
message : ` An expression is used in 'Fields to Set' in ${ expressionInFieldName . length === 1 ? 'field' : 'fields' } ${ expressionInFieldName . join ( ', ' ) } , did you mean to use it in the value instead? ` ,
whenToDisplay : 'beforeExecution' ,
location : 'outputPane' ,
} ) ;
}
}
// Split In Batches setup hints
if ( node . type === SPLIT_IN_BATCHES_NODE_TYPE ) {
const { connectionsBySourceNode } = workflow ;
const firstNodesInLoop = connectionsBySourceNode [ node . name ] ? . main [ 1 ] || [ ] ;
if ( ! firstNodesInLoop . length ) {
nodeHints . push ( {
message : "No nodes connected to the 'loop' output of this node" ,
whenToDisplay : 'beforeExecution' ,
location : 'outputPane' ,
} ) ;
} else {
for ( const nodeInConnection of firstNodesInLoop || [ ] ) {
const nodeChilds = workflow . getChildNodes ( nodeInConnection . node ) || [ ] ;
if ( ! nodeChilds . includes ( node . name ) ) {
nodeHints . push ( {
message :
"The last node in the branch of the 'loop' output must be connected back to the input of this node to loop correctly" ,
whenToDisplay : 'beforeExecution' ,
location : 'outputPane' ,
} ) ;
}
}
}
}
return nodeHints ;
}
2024-10-11 07:03:58 -07:00
/ * *
* Generate vertical insertion offsets for the given node count
*
* 2 nodes - > [ - nodeSize , nodeSize ] ,
* 3 nodes - > [ - nodeSize - 2 * gridSize , 0 , nodeSize + 2 * gridSize ] ,
* 4 nodes - > [ - 2 * nodeSize - 2 * gridSize , - nodeSize , nodeSize , 2 * nodeSize + 2 * gridSize ]
* 5 nodes - > [ - 2 * nodeSize - 2 * gridSize , - nodeSize , 0 , nodeSize , 2 * nodeSize + 2 * gridSize ]
* /
export function generateOffsets ( nodeCount : number , nodeSize : number , gridSize : number ) {
const offsets = [ ] ;
const half = Math . floor ( nodeCount / 2 ) ;
const isOdd = nodeCount % 2 === 1 ;
if ( nodeCount === 0 ) {
return [ ] ;
}
for ( let i = - half ; i <= half ; i ++ ) {
if ( i === 0 ) {
if ( isOdd ) {
offsets . push ( 0 ) ;
}
} else {
const offset = i * nodeSize + Math . sign ( i ) * ( Math . abs ( i ) - ( isOdd ? 0 : 1 ) ) * gridSize ;
offsets . push ( offset ) ;
}
}
return offsets ;
}