dify/web/app/components/workflow/hooks/use-nodes-interactions.ts
lyzno1 7c97ea4a9e fix: correct entry node alignment for wrapper offset
- Add ENTRY_NODE_WRAPPER_OFFSET constant (x: 0, y: 21) for Start/Trigger nodes
- Implement getNodeAlignPosition() to calculate actual inner node positions
- Fix horizontal/vertical helpline rendering to account for wrapper offset
- Fix snap-to-align logic to properly align inner nodes instead of wrapper
- Correct helpline width/height calculation by subtracting offset for entry nodes
- Ensure backward compatibility: only affects Start/Trigger nodes with EntryNodeContainer wrapper

This fix ensures that Start and Trigger nodes (which have an EntryNodeContainer wrapper
with status indicator) align based on their inner node boundaries rather than the wrapper
boundaries, matching the alignment behavior of regular nodes.
2025-09-30 18:36:49 +08:00

2014 lines
63 KiB
TypeScript

import type { MouseEvent } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type {
NodeDragHandler,
NodeMouseHandler,
OnConnect,
OnConnectEnd,
OnConnectStart,
ResizeParamsWithDirection,
} from 'reactflow'
import {
getConnectedEdges,
getOutgoers,
useReactFlow,
useStoreApi,
} from 'reactflow'
import type { PluginDefaultValue } from '../block-selector/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
import { useWorkflowStore } from '../store'
import {
CUSTOM_EDGE,
ITERATION_CHILDREN_Z_INDEX,
ITERATION_PADDING,
LOOP_CHILDREN_Z_INDEX,
LOOP_PADDING,
NODE_WIDTH_X_OFFSET,
X_OFFSET,
Y_OFFSET,
} from '../constants'
import {
genNewNodeTitleFromOld,
generateNewNode,
getNestedNodePosition,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap,
getTopLeftNodePosition,
} from '../utils'
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
import { useWorkflowHistoryStore } from '../workflow-history-store'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useHelpline } from './use-helpline'
import {
useNodesReadOnly,
useWorkflow,
useWorkflowReadOnly,
} from './use-workflow'
import {
WorkflowHistoryEvent,
useWorkflowHistory,
} from './use-workflow-history'
import { useNodesMetaData } from './use-nodes-meta-data'
import type { RAGPipelineVariables } from '@/models/pipeline'
import useInspectVarsCrud from './use-inspect-vars-crud'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
// Entry node deletion restriction has been removed to allow empty workflows
// Entry node (Start/Trigger) wrapper offsets for alignment
// Must match the values in use-helpline.ts
const ENTRY_NODE_WRAPPER_OFFSET = {
x: 0,
y: 21, // Adjusted based on visual testing feedback
} as const
export const useNodesInteractions = () => {
const { t } = useTranslation()
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getAfterNodesInSameBranch } = useWorkflow()
const { getNodesReadOnly } = useNodesReadOnly()
const { getWorkflowReadOnly } = useWorkflowReadOnly()
const { handleSetHelpline } = useHelpline()
const { handleNodeIterationChildDrag, handleNodeIterationChildrenCopy }
= useNodeIterationInteractions()
const { handleNodeLoopChildDrag, handleNodeLoopChildrenCopy }
= useNodeLoopInteractions()
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as {
x: number;
y: number;
})
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const { saveStateToHistory, undo, redo } = useWorkflowHistory()
const handleNodeDragStart = useCallback<NodeDragHandler>(
(_, node) => {
workflowStore.setState({ nodeAnimation: false })
if (getNodesReadOnly()) return
if (
node.type === CUSTOM_ITERATION_START_NODE
|| node.type === CUSTOM_NOTE_NODE
)
return
if (
node.type === CUSTOM_LOOP_START_NODE
|| node.type === CUSTOM_NOTE_NODE
)
return
dragNodeStartPosition.current = {
x: node.position.x,
y: node.position.y,
}
},
[workflowStore, getNodesReadOnly],
)
const handleNodeDrag = useCallback<NodeDragHandler>(
(e, node: Node) => {
if (getNodesReadOnly()) return
if (node.type === CUSTOM_ITERATION_START_NODE) return
if (node.type === CUSTOM_LOOP_START_NODE) return
const { getNodes, setNodes } = store.getState()
e.stopPropagation()
const nodes = getNodes()
const { restrictPosition } = handleNodeIterationChildDrag(node)
const { restrictPosition: restrictLoopPosition }
= handleNodeLoopChildDrag(node)
const { showHorizontalHelpLineNodes, showVerticalHelpLineNodes }
= handleSetHelpline(node)
const showHorizontalHelpLineNodesLength
= showHorizontalHelpLineNodes.length
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(n => n.id === node.id)!
// Check if current dragging node is an entry node
const isCurrentEntryNode = TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start
// X-axis alignment with offset consideration
if (showVerticalHelpLineNodesLength > 0) {
const targetNode = showVerticalHelpLineNodes[0]
const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
// Calculate the wrapper position needed to align the inner nodes
// Target inner position = target.position + target.offset
// Current inner position should equal target inner position
// So: current.position + current.offset = target.position + target.offset
// Therefore: current.position = target.position + target.offset - current.offset
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
currentNode.position.x = targetNode.position.x + targetOffset - currentOffset
}
else if (restrictPosition.x !== undefined) {
currentNode.position.x = restrictPosition.x
}
else if (restrictLoopPosition.x !== undefined) {
currentNode.position.x = restrictLoopPosition.x
}
else {
currentNode.position.x = node.position.x
}
// Y-axis alignment with offset consideration
if (showHorizontalHelpLineNodesLength > 0) {
const targetNode = showHorizontalHelpLineNodes[0]
const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
currentNode.position.y = targetNode.position.y + targetOffset - currentOffset
}
else if (restrictPosition.y !== undefined) {
currentNode.position.y = restrictPosition.y
}
else if (restrictLoopPosition.y !== undefined) {
currentNode.position.y = restrictLoopPosition.y
}
else {
currentNode.position.y = node.position.y
}
})
setNodes(newNodes)
},
[
getNodesReadOnly,
store,
handleNodeIterationChildDrag,
handleNodeLoopChildDrag,
handleSetHelpline,
],
)
const handleNodeDragStop = useCallback<NodeDragHandler>(
(_, node) => {
const { setHelpLineHorizontal, setHelpLineVertical }
= workflowStore.getState()
if (getNodesReadOnly()) return
const { x, y } = dragNodeStartPosition.current
if (!(x === node.position.x && y === node.position.y)) {
setHelpLineHorizontal()
setHelpLineVertical()
handleSyncWorkflowDraft()
if (x !== 0 && y !== 0) {
// selecting a note will trigger a drag stop event with x and y as 0
saveStateToHistory(WorkflowHistoryEvent.NodeDragStop, {
nodeId: node.id,
})
}
}
},
[
workflowStore,
getNodesReadOnly,
saveStateToHistory,
handleSyncWorkflowDraft,
],
)
const handleNodeEnter = useCallback<NodeMouseHandler>(
(_, node) => {
if (getNodesReadOnly()) return
if (
node.type === CUSTOM_NOTE_NODE
|| node.type === CUSTOM_ITERATION_START_NODE
)
return
if (
node.type === CUSTOM_LOOP_START_NODE
|| node.type === CUSTOM_NOTE_NODE
)
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const { connectingNodePayload, setEnteringNodePayload }
= workflowStore.getState()
if (connectingNodePayload) {
if (connectingNodePayload.nodeId === node.id) return
const connectingNode: Node = nodes.find(
n => n.id === connectingNodePayload.nodeId,
)!
const sameLevel = connectingNode.parentId === node.parentId
if (sameLevel) {
setEnteringNodePayload({
nodeId: node.id,
nodeData: node.data as VariableAssignerNodeType,
})
const fromType = connectingNodePayload.handleType
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (
n.id === node.id
&& fromType === 'source'
&& (node.data.type === BlockEnum.VariableAssigner
|| node.data.type === BlockEnum.VariableAggregator)
) {
if (!node.data.advanced_settings?.group_enabled)
n.data._isEntering = true
}
if (
n.id === node.id
&& fromType === 'target'
&& (connectingNode.data.type === BlockEnum.VariableAssigner
|| connectingNode.data.type === BlockEnum.VariableAggregator)
&& node.data.type !== BlockEnum.IfElse
&& node.data.type !== BlockEnum.QuestionClassifier
)
n.data._isEntering = true
})
})
setNodes(newNodes)
}
}
const newEdges = produce(edges, (draft) => {
const connectedEdges = getConnectedEdges([node], edges)
connectedEdges.forEach((edge) => {
const currentEdge = draft.find(e => e.id === edge.id)
if (currentEdge) currentEdge.data._connectedNodeIsHovering = true
})
})
setEdges(newEdges)
},
[store, workflowStore, getNodesReadOnly],
)
const handleNodeLeave = useCallback<NodeMouseHandler>(
(_, node) => {
if (getNodesReadOnly()) return
if (
node.type === CUSTOM_NOTE_NODE
|| node.type === CUSTOM_ITERATION_START_NODE
)
return
if (
node.type === CUSTOM_NOTE_NODE
|| node.type === CUSTOM_LOOP_START_NODE
)
return
const { setEnteringNodePayload } = workflowStore.getState()
setEnteringNodePayload(undefined)
const { getNodes, setNodes, edges, setEdges } = store.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._isEntering = false
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data._connectedNodeIsHovering = false
})
})
setEdges(newEdges)
},
[store, workflowStore, getNodesReadOnly],
)
const handleNodeSelect = useCallback(
(
nodeId: string,
cancelSelection?: boolean,
initShowLastRunTab?: boolean,
) => {
if (initShowLastRunTab)
workflowStore.setState({ initShowLastRunTab: true })
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const selectedNode = nodes.find(node => node.data.selected)
if (!cancelSelection && selectedNode?.id === nodeId) return
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (node.id === nodeId) node.data.selected = !cancelSelection
else node.data.selected = false
})
})
setNodes(newNodes)
const connectedEdges = getConnectedEdges(
[{ id: nodeId } as Node],
edges,
).map(edge => edge.id)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
if (connectedEdges.includes(edge.id)) {
edge.data = {
...edge.data,
_connectedNodeIsSelected: !cancelSelection,
}
}
else {
edge.data = {
...edge.data,
_connectedNodeIsSelected: false,
}
}
})
})
setEdges(newEdges)
handleSyncWorkflowDraft()
},
[store, handleSyncWorkflowDraft],
)
const handleNodeClick = useCallback<NodeMouseHandler>(
(_, node) => {
if (node.type === CUSTOM_ITERATION_START_NODE) return
if (node.type === CUSTOM_LOOP_START_NODE) return
if (node.data.type === BlockEnum.DataSourceEmpty) return
handleNodeSelect(node.id)
},
[handleNodeSelect],
)
const handleNodeConnect = useCallback<OnConnect>(
({ source, sourceHandle, target, targetHandle }) => {
if (source === target) return
if (getNodesReadOnly()) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const targetNode = nodes.find(node => node.id === target!)
const sourceNode = nodes.find(node => node.id === source!)
if (targetNode?.parentId !== sourceNode?.parentId) return
if (
sourceNode?.type === CUSTOM_NOTE_NODE
|| targetNode?.type === CUSTOM_NOTE_NODE
)
return
if (
edges.find(
edge =>
edge.source === source
&& edge.sourceHandle === sourceHandle
&& edge.target === target
&& edge.targetHandle === targetHandle,
)
)
return
const parendNode = nodes.find(node => node.id === targetNode?.parentId)
const isInIteration
= parendNode && parendNode.data.type === BlockEnum.Iteration
const isInLoop = !!parendNode && parendNode.data.type === BlockEnum.Loop
const newEdge = {
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
type: CUSTOM_EDGE,
source: source!,
target: target!,
sourceHandle,
targetHandle,
data: {
sourceType: nodes.find(node => node.id === source)!.data.type,
targetType: nodes.find(node => node.id === target)!.data.type,
isInIteration,
iteration_id: isInIteration ? targetNode?.parentId : undefined,
isInLoop,
loop_id: isInLoop ? targetNode?.parentId : undefined,
},
zIndex: targetNode?.parentId
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
}
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
[{ type: 'add', edge: newEdge }],
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
const newEdges = produce(edges, (draft) => {
draft.push(newEdge)
})
setNodes(newNodes)
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeConnect, {
nodeId: targetNode?.id,
})
},
[
getNodesReadOnly,
store,
workflowStore,
handleSyncWorkflowDraft,
saveStateToHistory,
],
)
const handleNodeConnectStart = useCallback<OnConnectStart>(
(_, { nodeId, handleType, handleId }) => {
if (getNodesReadOnly()) return
if (nodeId && handleType) {
const { setConnectingNodePayload } = workflowStore.getState()
const { getNodes } = store.getState()
const node = getNodes().find(n => n.id === nodeId)!
if (node.type === CUSTOM_NOTE_NODE) return
if (
node.data.type === BlockEnum.VariableAggregator
|| node.data.type === BlockEnum.VariableAssigner
)
if (handleType === 'target') return
setConnectingNodePayload({
nodeId,
nodeType: node.data.type,
handleType,
handleId,
})
}
},
[store, workflowStore, getNodesReadOnly],
)
const handleNodeConnectEnd = useCallback<OnConnectEnd>(
(e: any) => {
if (getNodesReadOnly()) return
const {
connectingNodePayload,
setConnectingNodePayload,
enteringNodePayload,
setEnteringNodePayload,
} = workflowStore.getState()
if (connectingNodePayload && enteringNodePayload) {
const { setShowAssignVariablePopup, hoveringAssignVariableGroupId }
= workflowStore.getState()
const { screenToFlowPosition } = reactflow
const { getNodes, setNodes } = store.getState()
const nodes = getNodes()
const fromHandleType = connectingNodePayload.handleType
const fromHandleId = connectingNodePayload.handleId
const fromNode = nodes.find(
n => n.id === connectingNodePayload.nodeId,
)!
const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
const toParentNode = nodes.find(n => n.id === toNode.parentId)
if (fromNode.parentId !== toNode.parentId) return
const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
if (
fromHandleType === 'source'
&& (toNode.data.type === BlockEnum.VariableAssigner
|| toNode.data.type === BlockEnum.VariableAggregator)
) {
const groupEnabled = toNode.data.advanced_settings?.group_enabled
const firstGroupId = toNode.data.advanced_settings?.groups[0].groupId
let handleId = 'target'
if (groupEnabled) {
if (hoveringAssignVariableGroupId)
handleId = hoveringAssignVariableGroupId
else handleId = firstGroupId
}
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (node.id === toNode.id) {
node.data._showAddVariablePopup = true
node.data._holdAddVariablePopup = true
}
})
})
setNodes(newNodes)
setShowAssignVariablePopup({
nodeId: fromNode.id,
nodeData: fromNode.data,
variableAssignerNodeId: toNode.id,
variableAssignerNodeData: toNode.data,
variableAssignerNodeHandleId: handleId,
parentNode: toParentNode,
x: x - toNode.positionAbsolute!.x,
y: y - toNode.positionAbsolute!.y,
})
handleNodeConnect({
source: fromNode.id,
sourceHandle: fromHandleId,
target: toNode.id,
targetHandle: 'target',
})
}
}
setConnectingNodePayload(undefined)
setEnteringNodePayload(undefined)
},
[store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow],
)
const { deleteNodeInspectorVars } = useInspectVarsCrud()
const handleNodeDelete = useCallback(
(nodeId: string) => {
if (getNodesReadOnly()) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
const currentNode = nodes[currentNodeIndex]
if (!currentNode) return
if (
nodesMetaDataMap?.[currentNode.data.type as BlockEnum]?.metaData
.isUndeletable
)
return
deleteNodeInspectorVars(nodeId)
if (currentNode.data.type === BlockEnum.Iteration) {
const iterationChildren = nodes.filter(
node => node.parentId === currentNode.id,
)
if (iterationChildren.length) {
if (currentNode.data._isBundled) {
iterationChildren.forEach((child) => {
handleNodeDelete(child.id)
})
return handleNodeDelete(nodeId)
}
else {
if (iterationChildren.length === 1) {
handleNodeDelete(iterationChildren[0].id)
handleNodeDelete(nodeId)
return
}
const { setShowConfirm, showConfirm } = workflowStore.getState()
if (!showConfirm) {
setShowConfirm({
title: t('workflow.nodes.iteration.deleteTitle'),
desc: t('workflow.nodes.iteration.deleteDesc') || '',
onConfirm: () => {
iterationChildren.forEach((child) => {
handleNodeDelete(child.id)
})
handleNodeDelete(nodeId)
handleSyncWorkflowDraft()
setShowConfirm(undefined)
},
})
return
}
}
}
}
if (currentNode.data.type === BlockEnum.Loop) {
const loopChildren = nodes.filter(
node => node.parentId === currentNode.id,
)
if (loopChildren.length) {
if (currentNode.data._isBundled) {
loopChildren.forEach((child) => {
handleNodeDelete(child.id)
})
return handleNodeDelete(nodeId)
}
else {
if (loopChildren.length === 1) {
handleNodeDelete(loopChildren[0].id)
handleNodeDelete(nodeId)
return
}
const { setShowConfirm, showConfirm } = workflowStore.getState()
if (!showConfirm) {
setShowConfirm({
title: t('workflow.nodes.loop.deleteTitle'),
desc: t('workflow.nodes.loop.deleteDesc') || '',
onConfirm: () => {
loopChildren.forEach((child) => {
handleNodeDelete(child.id)
})
handleNodeDelete(nodeId)
handleSyncWorkflowDraft()
setShowConfirm(undefined)
},
})
return
}
}
}
}
if (currentNode.data.type === BlockEnum.DataSource) {
const { id } = currentNode
const { ragPipelineVariables, setRagPipelineVariables }
= workflowStore.getState()
if (ragPipelineVariables && setRagPipelineVariables) {
const newRagPipelineVariables: RAGPipelineVariables = []
ragPipelineVariables.forEach((variable) => {
if (variable.belong_to_node_id === id) return
newRagPipelineVariables.push(variable)
})
setRagPipelineVariables(newRagPipelineVariables)
}
}
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
connectedEdges.map(edge => ({ type: 'remove', edge })),
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.id === currentNode.parentId) {
node.data._children = node.data._children?.filter(
child => child.nodeId !== nodeId,
)
}
})
draft.splice(currentNodeIndex, 1)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
return draft.filter(
edge =>
!connectedEdges.find(
connectedEdge => connectedEdge.id === edge.id,
),
)
})
setEdges(newEdges)
handleSyncWorkflowDraft()
if (currentNode.type === CUSTOM_NOTE_NODE) {
saveStateToHistory(WorkflowHistoryEvent.NoteDelete, {
nodeId: currentNode.id,
})
}
else {
saveStateToHistory(WorkflowHistoryEvent.NodeDelete, {
nodeId: currentNode.id,
})
}
},
[
getNodesReadOnly,
store,
handleSyncWorkflowDraft,
saveStateToHistory,
workflowStore,
t,
nodesMetaDataMap,
deleteNodeInspectorVars,
],
)
const handleNodeAdd = useCallback<OnNodeAdd>(
(
{
nodeType,
sourceHandle = 'source',
targetHandle = 'target',
pluginDefaultValue,
},
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
) => {
if (getNodesReadOnly()) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const { defaultValue } = nodesMetaDataMap![nodeType]
const { newNode, newIterationStartNode, newLoopStartNode }
= generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: {
...(defaultValue as any),
title:
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...pluginDefaultValue,
selected: true,
_showAddVariablePopup:
(nodeType === BlockEnum.VariableAssigner
|| nodeType === BlockEnum.VariableAggregator)
&& !!prevNodeId,
_holdAddVariablePopup: false,
},
position: {
x: 0,
y: 0,
},
})
if (prevNodeId && !nextNodeId) {
const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
const prevNode = nodes[prevNodeIndex]
const outgoers = getOutgoers(prevNode, nodes, edges).sort(
(a, b) => a.position.y - b.position.y,
)
const lastOutgoer = outgoers[outgoers.length - 1]
newNode.data._connectedTargetHandleIds
= nodeType === BlockEnum.DataSource ? [] : [targetHandle]
newNode.data._connectedSourceHandleIds = []
newNode.position = {
x: lastOutgoer
? lastOutgoer.position.x
: prevNode.position.x + prevNode.width! + X_OFFSET,
y: lastOutgoer
? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET
: prevNode.position.y,
}
newNode.parentId = prevNode.parentId
newNode.extent = prevNode.extent
const parentNode
= nodes.find(node => node.id === prevNode.parentId) || null
const isInIteration
= !!parentNode && parentNode.data.type === BlockEnum.Iteration
const isInLoop
= !!parentNode && parentNode.data.type === BlockEnum.Loop
if (prevNode.parentId) {
newNode.data.isInIteration = isInIteration
newNode.data.isInLoop = isInLoop
if (isInIteration) {
newNode.data.iteration_id = parentNode.id
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
if (isInLoop) {
newNode.data.loop_id = parentNode.id
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
}
if (
isInIteration
&& (newNode.data.type === BlockEnum.Answer
|| newNode.data.type === BlockEnum.Tool
|| newNode.data.type === BlockEnum.Assigner)
) {
const iterNodeData: IterationNodeType = parentNode.data
iterNodeData._isShowTips = true
}
if (
isInLoop
&& (newNode.data.type === BlockEnum.Answer
|| newNode.data.type === BlockEnum.Tool
|| newNode.data.type === BlockEnum.Assigner)
) {
const iterNodeData: IterationNodeType = parentNode.data
iterNodeData._isShowTips = true
}
}
let newEdge = null
if (nodeType !== BlockEnum.DataSource) {
newEdge = {
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: CUSTOM_EDGE,
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
target: newNode.id,
targetHandle,
data: {
sourceType: prevNode.data.type,
targetType: newNode.data.type,
isInIteration,
isInLoop,
iteration_id: isInIteration ? prevNode.parentId : undefined,
loop_id: isInLoop ? prevNode.parentId : undefined,
_connectedNodeIsSelected: true,
},
zIndex: prevNode.parentId
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
}
}
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
(newEdge ? [{ type: 'add', edge: newEdge }] : []),
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
node.data.selected = false
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (
node.data.type === BlockEnum.Iteration
&& prevNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
if (
node.data.type === BlockEnum.Loop
&& prevNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
})
draft.push(newNode)
if (newIterationStartNode) draft.push(newIterationStartNode)
if (newLoopStartNode) draft.push(newLoopStartNode)
})
if (
newNode.data.type === BlockEnum.VariableAssigner
|| newNode.data.type === BlockEnum.VariableAggregator
) {
const { setShowAssignVariablePopup } = workflowStore.getState()
setShowAssignVariablePopup({
nodeId: prevNode.id,
nodeData: prevNode.data,
variableAssignerNodeId: newNode.id,
variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
variableAssignerNodeHandleId: targetHandle,
parentNode: nodes.find(node => node.id === newNode.parentId),
x: -25,
y: 44,
})
}
const newEdges = produce(edges, (draft) => {
draft.forEach((item) => {
item.data = {
...item.data,
_connectedNodeIsSelected: false,
}
})
if (newEdge) draft.push(newEdge)
})
setNodes(newNodes)
setEdges(newEdges)
}
if (!prevNodeId && nextNodeId) {
const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
const nextNode = nodes[nextNodeIndex]!
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
)
newNode.data._connectedSourceHandleIds = [sourceHandle]
newNode.data._connectedTargetHandleIds = []
newNode.position = {
x: nextNode.position.x,
y: nextNode.position.y,
}
newNode.parentId = nextNode.parentId
newNode.extent = nextNode.extent
const parentNode
= nodes.find(node => node.id === nextNode.parentId) || null
const isInIteration
= !!parentNode && parentNode.data.type === BlockEnum.Iteration
const isInLoop
= !!parentNode && parentNode.data.type === BlockEnum.Loop
if (parentNode && nextNode.parentId) {
newNode.data.isInIteration = isInIteration
newNode.data.isInLoop = isInLoop
if (isInIteration) {
newNode.data.iteration_id = parentNode.id
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
if (isInLoop) {
newNode.data.loop_id = parentNode.id
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
}
}
let newEdge
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.LoopEnd
) {
newEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: CUSTOM_EDGE,
source: newNode.id,
sourceHandle,
target: nextNodeId,
targetHandle: nextNodeTargetHandle,
data: {
sourceType: newNode.data.type,
targetType: nextNode.data.type,
isInIteration,
isInLoop,
iteration_id: isInIteration ? nextNode.parentId : undefined,
loop_id: isInLoop ? nextNode.parentId : undefined,
_connectedNodeIsSelected: true,
},
zIndex: nextNode.parentId
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
}
}
let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
if (newEdge) {
nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
[{ type: 'add', edge: newEdge }],
nodes,
)
}
const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
const afterNodesInSameBranchIds = afterNodesInSameBranch.map(
node => node.id,
)
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
if (afterNodesInSameBranchIds.includes(node.id))
node.position.x += NODE_WIDTH_X_OFFSET
if (nodesConnectedSourceOrTargetHandleIdsMap?.[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (
node.data.type === BlockEnum.Iteration
&& nextNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
if (
node.data.type === BlockEnum.Iteration
&& node.data.start_node_id === nextNodeId
) {
node.data.start_node_id = newNode.id
node.data.startNodeType = newNode.data.type
}
if (
node.data.type === BlockEnum.Loop
&& nextNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
if (
node.data.type === BlockEnum.Loop
&& node.data.start_node_id === nextNodeId
) {
node.data.start_node_id = newNode.id
node.data.startNodeType = newNode.data.type
}
})
draft.push(newNode)
if (newIterationStartNode) draft.push(newIterationStartNode)
if (newLoopStartNode) draft.push(newLoopStartNode)
})
if (newEdge) {
const newEdges = produce(edges, (draft) => {
draft.forEach((item) => {
item.data = {
...item.data,
_connectedNodeIsSelected: false,
}
})
draft.push(newEdge)
})
setNodes(newNodes)
setEdges(newEdges)
}
else {
setNodes(newNodes)
}
}
if (prevNodeId && nextNodeId) {
const prevNode = nodes.find(node => node.id === prevNodeId)!
const nextNode = nodes.find(node => node.id === nextNodeId)!
newNode.data._connectedTargetHandleIds
= nodeType === BlockEnum.DataSource ? [] : [targetHandle]
newNode.data._connectedSourceHandleIds = [sourceHandle]
newNode.position = {
x: nextNode.position.x,
y: nextNode.position.y,
}
newNode.parentId = prevNode.parentId
newNode.extent = prevNode.extent
const parentNode
= nodes.find(node => node.id === prevNode.parentId) || null
const isInIteration
= !!parentNode && parentNode.data.type === BlockEnum.Iteration
const isInLoop
= !!parentNode && parentNode.data.type === BlockEnum.Loop
if (parentNode && prevNode.parentId) {
newNode.data.isInIteration = isInIteration
newNode.data.isInLoop = isInLoop
if (isInIteration) {
newNode.data.iteration_id = parentNode.id
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
if (isInLoop) {
newNode.data.loop_id = parentNode.id
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
}
}
const currentEdgeIndex = edges.findIndex(
edge => edge.source === prevNodeId && edge.target === nextNodeId,
)
let newPrevEdge = null
if (nodeType !== BlockEnum.DataSource) {
newPrevEdge = {
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: CUSTOM_EDGE,
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
target: newNode.id,
targetHandle,
data: {
sourceType: prevNode.data.type,
targetType: newNode.data.type,
isInIteration,
isInLoop,
iteration_id: isInIteration ? prevNode.parentId : undefined,
loop_id: isInLoop ? prevNode.parentId : undefined,
_connectedNodeIsSelected: true,
},
zIndex: prevNode.parentId
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
}
}
let newNextEdge: Edge | null = null
const nextNodeParentNode
= nodes.find(node => node.id === nextNode.parentId) || null
const isNextNodeInIteration
= !!nextNodeParentNode
&& nextNodeParentNode.data.type === BlockEnum.Iteration
const isNextNodeInLoop
= !!nextNodeParentNode
&& nextNodeParentNode.data.type === BlockEnum.Loop
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.LoopEnd
) {
newNextEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: CUSTOM_EDGE,
source: newNode.id,
sourceHandle,
target: nextNodeId,
targetHandle: nextNodeTargetHandle,
data: {
sourceType: newNode.data.type,
targetType: nextNode.data.type,
isInIteration: isNextNodeInIteration,
isInLoop: isNextNodeInLoop,
iteration_id: isNextNodeInIteration
? nextNode.parentId
: undefined,
loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
_connectedNodeIsSelected: true,
},
zIndex: nextNode.parentId
? isNextNodeInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
}
}
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
[
{ type: 'remove', edge: edges[currentEdgeIndex] },
...(newPrevEdge ? [{ type: 'add', edge: newPrevEdge }] : []),
...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []),
],
[...nodes, newNode],
)
const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
const afterNodesInSameBranchIds = afterNodesInSameBranch.map(
node => node.id,
)
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (afterNodesInSameBranchIds.includes(node.id))
node.position.x += NODE_WIDTH_X_OFFSET
if (
node.data.type === BlockEnum.Iteration
&& prevNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
if (
node.data.type === BlockEnum.Loop
&& prevNode.parentId === node.id
) {
node.data._children?.push({
nodeId: newNode.id,
nodeType: newNode.data.type,
})
}
})
draft.push(newNode)
if (newIterationStartNode) draft.push(newIterationStartNode)
if (newLoopStartNode) draft.push(newLoopStartNode)
})
setNodes(newNodes)
if (
newNode.data.type === BlockEnum.VariableAssigner
|| newNode.data.type === BlockEnum.VariableAggregator
) {
const { setShowAssignVariablePopup } = workflowStore.getState()
setShowAssignVariablePopup({
nodeId: prevNode.id,
nodeData: prevNode.data,
variableAssignerNodeId: newNode.id,
variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
variableAssignerNodeHandleId: targetHandle,
parentNode: nodes.find(node => node.id === newNode.parentId),
x: -25,
y: 44,
})
}
const newEdges = produce(edges, (draft) => {
draft.splice(currentEdgeIndex, 1)
draft.forEach((item) => {
item.data = {
...item.data,
_connectedNodeIsSelected: false,
}
})
if (newPrevEdge) draft.push(newPrevEdge)
if (newNextEdge) draft.push(newNextEdge)
})
setEdges(newEdges)
}
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
},
[
getNodesReadOnly,
store,
handleSyncWorkflowDraft,
saveStateToHistory,
workflowStore,
getAfterNodesInSameBranch,
nodesMetaDataMap,
],
)
const handleNodeChange = useCallback(
(
currentNodeId: string,
nodeType: BlockEnum,
sourceHandle: string,
pluginDefaultValue?: PluginDefaultValue,
) => {
if (getNodesReadOnly()) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === currentNodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const { defaultValue } = nodesMetaDataMap![nodeType]
const {
newNode: newCurrentNode,
newIterationStartNode,
newLoopStartNode,
} = generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: {
...(defaultValue as any),
title:
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...pluginDefaultValue,
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
selected: currentNode.data.selected,
isInIteration: currentNode.data.isInIteration,
isInLoop: currentNode.data.isInLoop,
iteration_id: currentNode.data.iteration_id,
loop_id: currentNode.data.loop_id,
},
position: {
x: currentNode.position.x,
y: currentNode.position.y,
},
parentId: currentNode.parentId,
extent: currentNode.extent,
zIndex: currentNode.zIndex,
})
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
connectedEdges.map(edge => ({ type: 'remove', edge })),
nodes,
)
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
const index = draft.findIndex(node => node.id === currentNodeId)
draft.splice(index, 1, newCurrentNode)
if (newIterationStartNode) draft.push(newIterationStartNode)
if (newLoopStartNode) draft.push(newLoopStartNode)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const filtered = draft.filter(
edge =>
!connectedEdges.find(
connectedEdge => connectedEdge.id === edge.id,
),
)
return filtered
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeChange, {
nodeId: currentNodeId,
})
},
[
getNodesReadOnly,
store,
handleSyncWorkflowDraft,
saveStateToHistory,
nodesMetaDataMap,
],
)
const handleNodesCancelSelected = useCallback(() => {
const { getNodes, setNodes } = store.getState()
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
})
})
setNodes(newNodes)
}, [store])
const handleNodeContextMenu = useCallback(
(e: MouseEvent, node: Node) => {
if (
node.type === CUSTOM_NOTE_NODE
|| node.type === CUSTOM_ITERATION_START_NODE
)
return
if (
node.type === CUSTOM_NOTE_NODE
|| node.type === CUSTOM_LOOP_START_NODE
)
return
e.preventDefault()
const container = document.querySelector('#workflow-container')
const { x, y } = container!.getBoundingClientRect()
workflowStore.setState({
nodeMenu: {
top: e.clientY - y,
left: e.clientX - x,
nodeId: node.id,
},
})
handleNodeSelect(node.id)
},
[workflowStore, handleNodeSelect],
)
const handleNodesCopy = useCallback(
(nodeId?: string) => {
if (getNodesReadOnly()) return
const { setClipboardElements } = workflowStore.getState()
const { getNodes } = store.getState()
const nodes = getNodes()
if (nodeId) {
// If nodeId is provided, copy that specific node
const nodeToCopy = nodes.find(
node =>
node.id === nodeId
&& node.data.type !== BlockEnum.Start
&& node.type !== CUSTOM_ITERATION_START_NODE
&& node.type !== CUSTOM_LOOP_START_NODE
&& node.data.type !== BlockEnum.LoopEnd
&& node.data.type !== BlockEnum.KnowledgeBase
&& node.data.type !== BlockEnum.DataSourceEmpty,
)
if (nodeToCopy) setClipboardElements([nodeToCopy])
}
else {
// If no nodeId is provided, fall back to the current behavior
const bundledNodes = nodes.filter((node) => {
if (!node.data._isBundled) return false
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
if (metaData.isSingleton) return false
return !node.data.isInIteration && !node.data.isInLoop
})
if (bundledNodes.length) {
setClipboardElements(bundledNodes)
return
}
const selectedNode = nodes.find((node) => {
if (!node.data.selected) return false
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
return !metaData.isSingleton
})
if (selectedNode) setClipboardElements([selectedNode])
}
},
[getNodesReadOnly, store, workflowStore],
)
const handleNodesPaste = useCallback(() => {
if (getNodesReadOnly()) return
const { clipboardElements, mousePosition } = workflowStore.getState()
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodesToPaste: Node[] = []
const edgesToPaste: Edge[] = []
const nodes = getNodes()
if (clipboardElements.length) {
const { x, y } = getTopLeftNodePosition(clipboardElements)
const { screenToFlowPosition } = reactflow
const currentPosition = screenToFlowPosition({
x: mousePosition.pageX,
y: mousePosition.pageY,
})
const offsetX = currentPosition.x - x
const offsetY = currentPosition.y - y
let idMapping: Record<string, string> = {}
clipboardElements.forEach((nodeToPaste, index) => {
const nodeType = nodeToPaste.data.type
const { newNode, newIterationStartNode, newLoopStartNode }
= generateNewNode({
type: nodeToPaste.type,
data: {
...nodesMetaDataMap![nodeType].defaultValue,
...nodeToPaste.data,
selected: false,
_isBundled: false,
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
title: genNewNodeTitleFromOld(nodeToPaste.data.title),
},
position: {
x: nodeToPaste.position.x + offsetX,
y: nodeToPaste.position.y + offsetY,
},
extent: nodeToPaste.extent,
zIndex: nodeToPaste.zIndex,
})
newNode.id = newNode.id + index
// This new node is movable and can be placed anywhere
let newChildren: Node[] = []
if (nodeToPaste.data.type === BlockEnum.Iteration) {
newIterationStartNode!.parentId = newNode.id;
(newNode.data as IterationNodeType).start_node_id
= newIterationStartNode!.id
const oldIterationStartNode = nodes.find(
n =>
n.parentId === nodeToPaste.id
&& n.type === CUSTOM_ITERATION_START_NODE,
)
idMapping[oldIterationStartNode!.id] = newIterationStartNode!.id
const { copyChildren, newIdMapping }
= handleNodeIterationChildrenCopy(
nodeToPaste.id,
newNode.id,
idMapping,
)
newChildren = copyChildren
idMapping = newIdMapping
newChildren.forEach((child) => {
newNode.data._children?.push({
nodeId: child.id,
nodeType: child.data.type,
})
})
newChildren.push(newIterationStartNode!)
}
else if (nodeToPaste.data.type === BlockEnum.Loop) {
newLoopStartNode!.parentId = newNode.id;
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id
newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
newChildren.forEach((child) => {
newNode.data._children?.push({
nodeId: child.id,
nodeType: child.data.type,
})
})
newChildren.push(newLoopStartNode!)
}
else {
// single node paste
const selectedNode = nodes.find(node => node.selected)
if (selectedNode) {
const commonNestedDisallowPasteNodes = [
// end node only can be placed outermost layer
BlockEnum.End,
]
// handle disallow paste node
if (commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type))
return
// handle paste to nested block
if (selectedNode.data.type === BlockEnum.Iteration) {
newNode.data.isInIteration = true
newNode.data.iteration_id = selectedNode.data.iteration_id
newNode.parentId = selectedNode.id
newNode.positionAbsolute = {
x: newNode.position.x,
y: newNode.position.y,
}
// set position base on parent node
newNode.position = getNestedNodePosition(newNode, selectedNode)
}
else if (selectedNode.data.type === BlockEnum.Loop) {
newNode.data.isInLoop = true
newNode.data.loop_id = selectedNode.data.loop_id
newNode.parentId = selectedNode.id
newNode.positionAbsolute = {
x: newNode.position.x,
y: newNode.position.y,
}
// set position base on parent node
newNode.position = getNestedNodePosition(newNode, selectedNode)
}
}
}
nodesToPaste.push(newNode)
if (newChildren.length) nodesToPaste.push(...newChildren)
})
// only handle edge when paste nested block
edges.forEach((edge) => {
const sourceId = idMapping[edge.source]
const targetId = idMapping[edge.target]
if (sourceId && targetId) {
const newEdge: Edge = {
...edge,
id: `${sourceId}-${edge.sourceHandle}-${targetId}-${edge.targetHandle}`,
source: sourceId,
target: targetId,
data: {
...edge.data,
_connectedNodeIsSelected: false,
},
}
edgesToPaste.push(newEdge)
}
})
setNodes([...nodes, ...nodesToPaste])
setEdges([...edges, ...edgesToPaste])
saveStateToHistory(WorkflowHistoryEvent.NodePaste, {
nodeId: nodesToPaste?.[0]?.id,
})
handleSyncWorkflowDraft()
}
}, [
getNodesReadOnly,
workflowStore,
store,
reactflow,
saveStateToHistory,
handleSyncWorkflowDraft,
handleNodeIterationChildrenCopy,
handleNodeLoopChildrenCopy,
nodesMetaDataMap,
])
const handleNodesDuplicate = useCallback(
(nodeId?: string) => {
if (getNodesReadOnly()) return
handleNodesCopy(nodeId)
handleNodesPaste()
},
[getNodesReadOnly, handleNodesCopy, handleNodesPaste],
)
const handleNodesDelete = useCallback(() => {
if (getNodesReadOnly()) return
const { getNodes, edges } = store.getState()
const nodes = getNodes()
const bundledNodes = nodes.filter(
node => node.data._isBundled,
)
if (bundledNodes.length) {
bundledNodes.forEach(node => handleNodeDelete(node.id))
return
}
const edgeSelected = edges.some(edge => edge.selected)
if (edgeSelected) return
const selectedNode = nodes.find(
node => node.data.selected,
)
if (selectedNode) handleNodeDelete(selectedNode.id)
}, [store, getNodesReadOnly, handleNodeDelete])
const handleNodeResize = useCallback(
(nodeId: string, params: ResizeParamsWithDirection) => {
if (getNodesReadOnly()) return
const { getNodes, setNodes } = store.getState()
const { x, y, width, height } = params
const nodes = getNodes()
const currentNode = nodes.find(n => n.id === nodeId)!
const childrenNodes = nodes.filter(n =>
currentNode.data._children?.find((c: any) => c.nodeId === n.id),
)
let rightNode: Node
let bottomNode: Node
childrenNodes.forEach((n) => {
if (rightNode) {
if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
rightNode = n
}
else {
rightNode = n
}
if (bottomNode) {
if (
n.position.y + n.height!
> bottomNode.position.y + bottomNode.height!
)
bottomNode = n
}
else {
bottomNode = n
}
})
if (rightNode! && bottomNode!) {
const parentNode = nodes.find(n => n.id === rightNode.parentId)
const paddingMap
= parentNode?.data.type === BlockEnum.Iteration
? ITERATION_PADDING
: LOOP_PADDING
if (width < rightNode!.position.x + rightNode.width! + paddingMap.right)
return
if (
height
< bottomNode.position.y + bottomNode.height! + paddingMap.bottom
)
return
}
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (n.id === nodeId) {
n.data.width = width
n.data.height = height
n.width = width
n.height = height
n.position.x = x
n.position.y = y
}
})
})
setNodes(newNodes)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeResize, { nodeId })
},
[getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory],
)
const handleNodeDisconnect = useCallback(
(nodeId: string) => {
if (getNodesReadOnly()) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === nodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
connectedEdges.map(edge => ({ type: 'remove', edge })),
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
return draft.filter(
edge =>
!connectedEdges.find(
connectedEdge => connectedEdge.id === edge.id,
),
)
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
},
[store, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory],
)
const handleHistoryBack = useCallback(() => {
if (getNodesReadOnly() || getWorkflowReadOnly()) return
const { setEdges, setNodes } = store.getState()
undo()
const { edges, nodes } = workflowHistoryStore.getState()
if (edges.length === 0 && nodes.length === 0) return
setEdges(edges)
setNodes(nodes)
}, [
store,
undo,
workflowHistoryStore,
getNodesReadOnly,
getWorkflowReadOnly,
])
const handleHistoryForward = useCallback(() => {
if (getNodesReadOnly() || getWorkflowReadOnly()) return
const { setEdges, setNodes } = store.getState()
redo()
const { edges, nodes } = workflowHistoryStore.getState()
if (edges.length === 0 && nodes.length === 0) return
setEdges(edges)
setNodes(nodes)
}, [
redo,
store,
workflowHistoryStore,
getNodesReadOnly,
getWorkflowReadOnly,
])
const [isDimming, setIsDimming] = useState(false)
/** Add opacity-30 to all nodes except the nodeId */
const dimOtherNodes = useCallback(() => {
if (isDimming) return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const selectedNode = nodes.find(n => n.data.selected)
if (!selectedNode) return
setIsDimming(true)
// const workflowNodes = useStore(s => s.getNodes())
const workflowNodes = nodes
const usedVars = getNodeUsedVars(selectedNode)
const dependencyNodes: Node[] = []
usedVars.forEach((valueSelector) => {
const node = workflowNodes.find(node => node.id === valueSelector?.[0])
if (node)
if (!dependencyNodes.includes(node)) dependencyNodes.push(node)
})
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
const node = outgoers[currIdx]
const outgoersForNode = getOutgoers(node, nodes as Node[], edges)
outgoersForNode.forEach((item) => {
const existed = outgoers.some(v => v.id === item.id)
if (!existed) outgoers.push(item)
})
}
const dependentNodes: Node[] = []
outgoers.forEach((node) => {
const usedVars = getNodeUsedVars(node)
const used = usedVars.some(v => v?.[0] === selectedNode.id)
if (used) {
const existed = dependentNodes.some(v => v.id === node.id)
if (!existed) dependentNodes.push(node)
}
})
const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode]
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
const dimNode = dimNodes.find(v => v.id === n.id)
if (!dimNode) n.data._dimmed = true
})
})
setNodes(newNodes)
const tempEdges: Edge[] = []
dependencyNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${n.id}-source-${selectedNode.id}-target`,
type: CUSTOM_EDGE,
source: n.id,
sourceHandle: 'source_tmp',
target: selectedNode.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: n.data.type,
targetType: selectedNode.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
dependentNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${selectedNode.id}-source-${n.id}-target`,
type: CUSTOM_EDGE,
source: selectedNode.id,
sourceHandle: 'source_tmp',
target: n.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: selectedNode.data.type,
targetType: n.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
const newEdges = produce(edges, (draft) => {
draft.forEach((e) => {
e.data._dimmed = true
})
draft.push(...tempEdges)
})
setEdges(newEdges)
}, [isDimming, store])
/** Restore all nodes to full opacity */
const undimAllNodes = useCallback(() => {
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
setIsDimming(false)
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
n.data._dimmed = false
})
})
setNodes(newNodes)
const newEdges = produce(
edges.filter(e => !e.data._isTemp),
(draft) => {
draft.forEach((e) => {
e.data._dimmed = false
})
},
)
setEdges(newEdges)
}, [store])
return {
handleNodeDragStart,
handleNodeDrag,
handleNodeDragStop,
handleNodeEnter,
handleNodeLeave,
handleNodeSelect,
handleNodeClick,
handleNodeConnect,
handleNodeConnectStart,
handleNodeConnectEnd,
handleNodeDelete,
handleNodeChange,
handleNodeAdd,
handleNodesCancelSelected,
handleNodeContextMenu,
handleNodesCopy,
handleNodesPaste,
handleNodesDuplicate,
handleNodesDelete,
handleNodeResize,
handleNodeDisconnect,
handleHistoryBack,
handleHistoryForward,
dimOtherNodes,
undimAllNodes,
}
}