mirror of
https://github.com/langgenius/dify.git
synced 2026-02-15 07:24:15 +08:00
- Updated `checkMakeGroupAvailability` to include a check for existing group nodes, preventing group creation if a group node is already selected. - Modified `useMakeGroupAvailability` and `useNodesInteractions` hooks to incorporate the new group node check, ensuring accurate group creation logic. - Adjusted UI rendering logic in the workflow panel to conditionally display elements based on node type, specifically for group nodes.
2876 lines
90 KiB
TypeScript
2876 lines
90 KiB
TypeScript
import type { MouseEvent } from 'react'
|
|
import type {
|
|
NodeDragHandler,
|
|
NodeMouseHandler,
|
|
OnConnect,
|
|
OnConnectEnd,
|
|
OnConnectStart,
|
|
ResizeParamsWithDirection,
|
|
} from 'reactflow'
|
|
import type { PluginDefaultValue } from '../block-selector/types'
|
|
import type { GroupHandler, GroupMember, GroupNodeData } from '../nodes/group/types'
|
|
import type { IterationNodeType } from '../nodes/iteration/types'
|
|
import type { LoopNodeType } from '../nodes/loop/types'
|
|
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
|
import type { Edge, Node, OnNodeAdd } from '../types'
|
|
import type { RAGPipelineVariables } from '@/models/pipeline'
|
|
import { produce } from 'immer'
|
|
import { useCallback, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
getConnectedEdges,
|
|
getOutgoers,
|
|
useReactFlow,
|
|
useStoreApi,
|
|
} from 'reactflow'
|
|
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 { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
|
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
|
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
|
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
|
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
|
|
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
|
import { useWorkflowStore } from '../store'
|
|
import { BlockEnum, isTriggerNode } from '../types'
|
|
import {
|
|
generateNewNode,
|
|
genNewNodeTitleFromOld,
|
|
getNestedNodePosition,
|
|
getNodeCustomTypeByNodeDataType,
|
|
getNodesConnectedSourceOrTargetHandleIdsMap,
|
|
getTopLeftNodePosition,
|
|
} from '../utils'
|
|
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
|
import { useAutoGenerateWebhookUrl } from './use-auto-generate-webhook-url'
|
|
import { useHelpline } from './use-helpline'
|
|
import useInspectVarsCrud from './use-inspect-vars-crud'
|
|
import { checkMakeGroupAvailability } from './use-make-group'
|
|
import { useNodesMetaData } from './use-nodes-meta-data'
|
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
|
import {
|
|
useNodesReadOnly,
|
|
useWorkflow,
|
|
useWorkflowReadOnly,
|
|
} from './use-workflow'
|
|
import {
|
|
useWorkflowHistory,
|
|
WorkflowHistoryEvent,
|
|
} from './use-workflow-history'
|
|
|
|
// 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
|
|
|
|
/**
|
|
* Parse group handler id to get original node id and sourceHandle
|
|
* Handler id format: `${nodeId}-${sourceHandle}`
|
|
*/
|
|
function parseGroupHandlerId(handlerId: string): { originalNodeId: string, originalSourceHandle: string } {
|
|
const lastDashIndex = handlerId.lastIndexOf('-')
|
|
return {
|
|
originalNodeId: handlerId.substring(0, lastDashIndex),
|
|
originalSourceHandle: handlerId.substring(lastDashIndex + 1),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a pair of edges for group node connections:
|
|
* - realEdge: hidden edge from original node to target (persisted to backend)
|
|
* - uiEdge: visible temp edge from group to target (UI-only, not persisted)
|
|
*/
|
|
function createGroupEdgePair(params: {
|
|
groupNodeId: string
|
|
handlerId: string
|
|
targetNodeId: string
|
|
targetHandle: string
|
|
nodes: Node[]
|
|
baseEdgeData?: Partial<Edge['data']>
|
|
zIndex?: number
|
|
}): { realEdge: Edge, uiEdge: Edge } | null {
|
|
const { groupNodeId, handlerId, targetNodeId, targetHandle, nodes, baseEdgeData = {}, zIndex = 0 } = params
|
|
|
|
const groupNode = nodes.find(node => node.id === groupNodeId)
|
|
const groupData = groupNode?.data as GroupNodeData | undefined
|
|
const handler = groupData?.handlers?.find(h => h.id === handlerId)
|
|
|
|
let originalNodeId: string
|
|
let originalSourceHandle: string
|
|
|
|
if (handler?.nodeId && handler?.sourceHandle) {
|
|
originalNodeId = handler.nodeId
|
|
originalSourceHandle = handler.sourceHandle
|
|
}
|
|
else {
|
|
const parsed = parseGroupHandlerId(handlerId)
|
|
originalNodeId = parsed.originalNodeId
|
|
originalSourceHandle = parsed.originalSourceHandle
|
|
}
|
|
|
|
const originalNode = nodes.find(node => node.id === originalNodeId)
|
|
const targetNode = nodes.find(node => node.id === targetNodeId)
|
|
|
|
if (!originalNode || !targetNode)
|
|
return null
|
|
|
|
// Create the real edge (from original node to target) - hidden because original node is in group
|
|
const realEdge: Edge = {
|
|
id: `${originalNodeId}-${originalSourceHandle}-${targetNodeId}-${targetHandle}`,
|
|
type: CUSTOM_EDGE,
|
|
source: originalNodeId,
|
|
sourceHandle: originalSourceHandle,
|
|
target: targetNodeId,
|
|
targetHandle,
|
|
hidden: true,
|
|
data: {
|
|
...baseEdgeData,
|
|
sourceType: originalNode.data.type,
|
|
targetType: targetNode.data.type,
|
|
_hiddenInGroupId: groupNodeId,
|
|
},
|
|
zIndex,
|
|
}
|
|
|
|
// Create the UI edge (from group to target) - temporary, not persisted to backend
|
|
const uiEdge: Edge = {
|
|
id: `${groupNodeId}-${handlerId}-${targetNodeId}-${targetHandle}`,
|
|
type: CUSTOM_EDGE,
|
|
source: groupNodeId,
|
|
sourceHandle: handlerId,
|
|
target: targetNodeId,
|
|
targetHandle,
|
|
data: {
|
|
...baseEdgeData,
|
|
sourceType: BlockEnum.Group,
|
|
targetType: targetNode.data.type,
|
|
_isTemp: true,
|
|
},
|
|
zIndex,
|
|
}
|
|
|
|
return { realEdge, uiEdge }
|
|
}
|
|
|
|
function createGroupInboundEdges(params: {
|
|
sourceNodeId: string
|
|
sourceHandle: string
|
|
groupNodeId: string
|
|
groupData: GroupNodeData
|
|
nodes: Node[]
|
|
baseEdgeData?: Partial<Edge['data']>
|
|
zIndex?: number
|
|
}): { realEdges: Edge[], uiEdge: Edge } | null {
|
|
const { sourceNodeId, sourceHandle, groupNodeId, groupData, nodes, baseEdgeData = {}, zIndex = 0 } = params
|
|
|
|
const sourceNode = nodes.find(node => node.id === sourceNodeId)
|
|
const headNodeIds = groupData.headNodeIds || []
|
|
|
|
if (!sourceNode || headNodeIds.length === 0)
|
|
return null
|
|
|
|
const realEdges: Edge[] = headNodeIds.map((headNodeId) => {
|
|
const headNode = nodes.find(node => node.id === headNodeId)
|
|
return {
|
|
id: `${sourceNodeId}-${sourceHandle}-${headNodeId}-target`,
|
|
type: CUSTOM_EDGE,
|
|
source: sourceNodeId,
|
|
sourceHandle,
|
|
target: headNodeId,
|
|
targetHandle: 'target',
|
|
hidden: true,
|
|
data: {
|
|
...baseEdgeData,
|
|
sourceType: sourceNode.data.type,
|
|
targetType: headNode?.data.type,
|
|
_hiddenInGroupId: groupNodeId,
|
|
},
|
|
zIndex,
|
|
} as Edge
|
|
})
|
|
|
|
const uiEdge: Edge = {
|
|
id: `${sourceNodeId}-${sourceHandle}-${groupNodeId}-target`,
|
|
type: CUSTOM_EDGE,
|
|
source: sourceNodeId,
|
|
sourceHandle,
|
|
target: groupNodeId,
|
|
targetHandle: 'target',
|
|
data: {
|
|
...baseEdgeData,
|
|
sourceType: sourceNode.data.type,
|
|
targetType: BlockEnum.Group,
|
|
_isTemp: true,
|
|
},
|
|
zIndex,
|
|
}
|
|
|
|
return { realEdges, uiEdge }
|
|
}
|
|
|
|
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 autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
|
|
|
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 = isTriggerNode(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 = isTriggerNode(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 = isTriggerNode(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
|
|
if (node.data._pluginInstallLocked)
|
|
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
|
|
}
|
|
|
|
// Check if source is a group node - need special handling
|
|
const isSourceGroup = sourceNode?.data.type === BlockEnum.Group
|
|
|
|
if (isSourceGroup && sourceHandle && target && targetHandle) {
|
|
const { originalNodeId, originalSourceHandle } = parseGroupHandlerId(sourceHandle)
|
|
|
|
// Check if real edge already exists
|
|
if (edges.find(edge =>
|
|
edge.source === originalNodeId
|
|
&& edge.sourceHandle === originalSourceHandle
|
|
&& edge.target === target
|
|
&& edge.targetHandle === targetHandle,
|
|
)) {
|
|
return
|
|
}
|
|
|
|
const parentNode = nodes.find(node => node.id === targetNode?.parentId)
|
|
const isInIteration = parentNode && parentNode.data.type === BlockEnum.Iteration
|
|
const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
|
|
|
|
const edgePair = createGroupEdgePair({
|
|
groupNodeId: source!,
|
|
handlerId: sourceHandle,
|
|
targetNodeId: target,
|
|
targetHandle,
|
|
nodes,
|
|
baseEdgeData: {
|
|
isInIteration,
|
|
iteration_id: isInIteration ? targetNode?.parentId : undefined,
|
|
isInLoop,
|
|
loop_id: isInLoop ? targetNode?.parentId : undefined,
|
|
},
|
|
})
|
|
|
|
if (!edgePair)
|
|
return
|
|
|
|
const { realEdge, uiEdge } = edgePair
|
|
|
|
// Update connected handle ids for the original node
|
|
const nodesConnectedSourceOrTargetHandleIdsMap
|
|
= getNodesConnectedSourceOrTargetHandleIdsMap(
|
|
[{ type: 'add', edge: realEdge }],
|
|
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(realEdge)
|
|
draft.push(uiEdge)
|
|
})
|
|
|
|
setNodes(newNodes)
|
|
setEdges(newEdges)
|
|
|
|
handleSyncWorkflowDraft()
|
|
saveStateToHistory(WorkflowHistoryEvent.NodeConnect, {
|
|
nodeId: targetNode?.id,
|
|
})
|
|
return
|
|
}
|
|
|
|
const isTargetGroup = targetNode?.data.type === BlockEnum.Group
|
|
|
|
if (isTargetGroup && source && sourceHandle) {
|
|
const groupData = targetNode.data as GroupNodeData
|
|
const headNodeIds = groupData.headNodeIds || []
|
|
|
|
if (edges.find(edge =>
|
|
edge.source === source
|
|
&& edge.sourceHandle === sourceHandle
|
|
&& edge.target === target
|
|
&& edge.targetHandle === targetHandle,
|
|
)) {
|
|
return
|
|
}
|
|
|
|
const parentNode = nodes.find(node => node.id === sourceNode?.parentId)
|
|
const isInIteration = parentNode && parentNode.data.type === BlockEnum.Iteration
|
|
const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
|
|
|
|
const inboundResult = createGroupInboundEdges({
|
|
sourceNodeId: source,
|
|
sourceHandle,
|
|
groupNodeId: target!,
|
|
groupData,
|
|
nodes,
|
|
baseEdgeData: {
|
|
isInIteration,
|
|
iteration_id: isInIteration ? sourceNode?.parentId : undefined,
|
|
isInLoop,
|
|
loop_id: isInLoop ? sourceNode?.parentId : undefined,
|
|
},
|
|
})
|
|
|
|
if (!inboundResult)
|
|
return
|
|
|
|
const { realEdges, uiEdge } = inboundResult
|
|
|
|
const edgeChanges = realEdges.map(edge => ({ type: 'add' as const, edge }))
|
|
const nodesConnectedSourceOrTargetHandleIdsMap
|
|
= getNodesConnectedSourceOrTargetHandleIdsMap(edgeChanges, 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) => {
|
|
realEdges.forEach((edge) => {
|
|
draft.push(edge)
|
|
})
|
|
draft.push(uiEdge)
|
|
})
|
|
|
|
setNodes(newNodes)
|
|
setEdges(newEdges)
|
|
|
|
handleSyncWorkflowDraft()
|
|
saveStateToHistory(WorkflowHistoryEvent.NodeConnect, {
|
|
nodeId: headNodeIds[0],
|
|
})
|
|
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('nodes.iteration.deleteTitle', { ns: 'workflow' }),
|
|
desc: t('nodes.iteration.deleteDesc', { ns: 'workflow' }) || '',
|
|
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('nodes.loop.deleteTitle', { ns: 'workflow' }),
|
|
desc: t('nodes.loop.deleteDesc', { ns: 'workflow' }) || '',
|
|
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
|
|
}
|
|
}
|
|
|
|
// Check if prevNode is a group node - need special handling
|
|
const isPrevNodeGroup = prevNode.data.type === BlockEnum.Group
|
|
let newEdge: Edge | null = null
|
|
let newUiEdge: Edge | null = null
|
|
|
|
if (isPrevNodeGroup && prevNodeSourceHandle && nodeType !== BlockEnum.DataSource) {
|
|
const edgePair = createGroupEdgePair({
|
|
groupNodeId: prevNodeId,
|
|
handlerId: prevNodeSourceHandle,
|
|
targetNodeId: newNode.id,
|
|
targetHandle,
|
|
nodes: [...nodes, newNode],
|
|
baseEdgeData: {
|
|
isInIteration,
|
|
isInLoop,
|
|
iteration_id: isInIteration ? prevNode.parentId : undefined,
|
|
loop_id: isInLoop ? prevNode.parentId : undefined,
|
|
_connectedNodeIsSelected: true,
|
|
},
|
|
})
|
|
|
|
if (edgePair) {
|
|
newEdge = edgePair.realEdge
|
|
newUiEdge = edgePair.uiEdge
|
|
}
|
|
}
|
|
else if (nodeType !== BlockEnum.DataSource) {
|
|
// Normal case: prevNode is not a group
|
|
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 edgesToAdd = [newEdge, newUiEdge].filter(Boolean).map(edge => ({ type: 'add' as const, edge: edge! }))
|
|
const nodesConnectedSourceOrTargetHandleIdsMap
|
|
= getNodesConnectedSourceOrTargetHandleIdsMap(
|
|
edgesToAdd,
|
|
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)
|
|
if (newUiEdge)
|
|
draft.push(newUiEdge)
|
|
})
|
|
|
|
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) => 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
|
|
}
|
|
}
|
|
|
|
// Check if prevNode is a group node - need special handling
|
|
const isPrevNodeGroup = prevNode.data.type === BlockEnum.Group
|
|
let newPrevEdge: Edge | null = null
|
|
let newPrevUiEdge: Edge | null = null
|
|
const edgesToRemove: string[] = []
|
|
|
|
if (isPrevNodeGroup && prevNodeSourceHandle && nodeType !== BlockEnum.DataSource) {
|
|
const { originalNodeId, originalSourceHandle } = parseGroupHandlerId(prevNodeSourceHandle)
|
|
|
|
// Find edges to remove: both hidden real edge and UI temp edge from group to nextNode
|
|
const hiddenEdge = edges.find(
|
|
edge => edge.source === originalNodeId
|
|
&& edge.sourceHandle === originalSourceHandle
|
|
&& edge.target === nextNodeId,
|
|
)
|
|
const uiTempEdge = edges.find(
|
|
edge => edge.source === prevNodeId
|
|
&& edge.sourceHandle === prevNodeSourceHandle
|
|
&& edge.target === nextNodeId,
|
|
)
|
|
if (hiddenEdge)
|
|
edgesToRemove.push(hiddenEdge.id)
|
|
if (uiTempEdge)
|
|
edgesToRemove.push(uiTempEdge.id)
|
|
|
|
const edgePair = createGroupEdgePair({
|
|
groupNodeId: prevNodeId,
|
|
handlerId: prevNodeSourceHandle,
|
|
targetNodeId: newNode.id,
|
|
targetHandle,
|
|
nodes: [...nodes, newNode],
|
|
baseEdgeData: {
|
|
isInIteration,
|
|
isInLoop,
|
|
iteration_id: isInIteration ? prevNode.parentId : undefined,
|
|
loop_id: isInLoop ? prevNode.parentId : undefined,
|
|
_connectedNodeIsSelected: true,
|
|
},
|
|
})
|
|
|
|
if (edgePair) {
|
|
newPrevEdge = edgePair.realEdge
|
|
newPrevUiEdge = edgePair.uiEdge
|
|
}
|
|
}
|
|
else {
|
|
const isNextNodeGroupForRemoval = nextNode.data.type === BlockEnum.Group
|
|
|
|
if (isNextNodeGroupForRemoval) {
|
|
const groupData = nextNode.data as GroupNodeData
|
|
const headNodeIds = groupData.headNodeIds || []
|
|
|
|
headNodeIds.forEach((headNodeId) => {
|
|
const realEdge = edges.find(
|
|
edge => edge.source === prevNodeId
|
|
&& edge.sourceHandle === prevNodeSourceHandle
|
|
&& edge.target === headNodeId,
|
|
)
|
|
if (realEdge)
|
|
edgesToRemove.push(realEdge.id)
|
|
})
|
|
|
|
const uiEdge = edges.find(
|
|
edge => edge.source === prevNodeId
|
|
&& edge.sourceHandle === prevNodeSourceHandle
|
|
&& edge.target === nextNodeId,
|
|
)
|
|
if (uiEdge)
|
|
edgesToRemove.push(uiEdge.id)
|
|
}
|
|
else {
|
|
const currentEdge = edges.find(
|
|
edge => edge.source === prevNodeId && edge.target === nextNodeId,
|
|
)
|
|
if (currentEdge)
|
|
edgesToRemove.push(currentEdge.id)
|
|
}
|
|
|
|
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
|
|
let newNextUiEdge: Edge | null = null
|
|
const newNextRealEdges: Edge[] = []
|
|
|
|
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
|
|
|
|
const isNextNodeGroup = nextNode.data.type === BlockEnum.Group
|
|
|
|
if (
|
|
nodeType !== BlockEnum.IfElse
|
|
&& nodeType !== BlockEnum.QuestionClassifier
|
|
&& nodeType !== BlockEnum.LoopEnd
|
|
) {
|
|
if (isNextNodeGroup) {
|
|
const groupData = nextNode.data as GroupNodeData
|
|
const headNodeIds = groupData.headNodeIds || []
|
|
|
|
headNodeIds.forEach((headNodeId) => {
|
|
const headNode = nodes.find(node => node.id === headNodeId)
|
|
newNextRealEdges.push({
|
|
id: `${newNode.id}-${sourceHandle}-${headNodeId}-target`,
|
|
type: CUSTOM_EDGE,
|
|
source: newNode.id,
|
|
sourceHandle,
|
|
target: headNodeId,
|
|
targetHandle: 'target',
|
|
hidden: true,
|
|
data: {
|
|
sourceType: newNode.data.type,
|
|
targetType: headNode?.data.type,
|
|
isInIteration: isNextNodeInIteration,
|
|
isInLoop: isNextNodeInLoop,
|
|
iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined,
|
|
loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
|
|
_hiddenInGroupId: nextNodeId,
|
|
_connectedNodeIsSelected: true,
|
|
},
|
|
zIndex: nextNode.parentId
|
|
? isNextNodeInIteration
|
|
? ITERATION_CHILDREN_Z_INDEX
|
|
: LOOP_CHILDREN_Z_INDEX
|
|
: 0,
|
|
} as Edge)
|
|
})
|
|
|
|
newNextUiEdge = {
|
|
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-target`,
|
|
type: CUSTOM_EDGE,
|
|
source: newNode.id,
|
|
sourceHandle,
|
|
target: nextNodeId,
|
|
targetHandle: 'target',
|
|
data: {
|
|
sourceType: newNode.data.type,
|
|
targetType: BlockEnum.Group,
|
|
isInIteration: isNextNodeInIteration,
|
|
isInLoop: isNextNodeInLoop,
|
|
iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined,
|
|
loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
|
|
_isTemp: true,
|
|
_connectedNodeIsSelected: true,
|
|
},
|
|
zIndex: nextNode.parentId
|
|
? isNextNodeInIteration
|
|
? ITERATION_CHILDREN_Z_INDEX
|
|
: LOOP_CHILDREN_Z_INDEX
|
|
: 0,
|
|
}
|
|
}
|
|
else {
|
|
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 edgeChanges = [
|
|
...edgesToRemove.map(id => ({ type: 'remove' as const, edge: edges.find(e => e.id === id)! })).filter(c => c.edge),
|
|
...(newPrevEdge ? [{ type: 'add' as const, edge: newPrevEdge }] : []),
|
|
...(newPrevUiEdge ? [{ type: 'add' as const, edge: newPrevUiEdge }] : []),
|
|
...(newNextEdge ? [{ type: 'add' as const, edge: newNextEdge }] : []),
|
|
...newNextRealEdges.map(edge => ({ type: 'add' as const, edge })),
|
|
...(newNextUiEdge ? [{ type: 'add' as const, edge: newNextUiEdge }] : []),
|
|
]
|
|
const nodesConnectedSourceOrTargetHandleIdsMap
|
|
= getNodesConnectedSourceOrTargetHandleIdsMap(
|
|
edgeChanges,
|
|
[...nodes, newNode],
|
|
)
|
|
|
|
const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
|
|
const afterNodesInSameBranchIds = afterNodesInSameBranch.map(
|
|
(node: 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) => {
|
|
const filteredDraft = draft.filter(edge => !edgesToRemove.includes(edge.id))
|
|
draft.length = 0
|
|
draft.push(...filteredDraft)
|
|
|
|
draft.forEach((item) => {
|
|
item.data = {
|
|
...item.data,
|
|
_connectedNodeIsSelected: false,
|
|
}
|
|
})
|
|
if (newPrevEdge)
|
|
draft.push(newPrevEdge)
|
|
if (newPrevUiEdge)
|
|
draft.push(newPrevUiEdge)
|
|
if (newNextEdge)
|
|
draft.push(newNextEdge)
|
|
newNextRealEdges.forEach((edge) => {
|
|
draft.push(edge)
|
|
})
|
|
if (newNextUiEdge)
|
|
draft.push(newNextUiEdge)
|
|
})
|
|
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)
|
|
if (nodeType === BlockEnum.TriggerWebhook) {
|
|
handleSyncWorkflowDraft(true, true, {
|
|
onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
|
|
})
|
|
}
|
|
else {
|
|
handleSyncWorkflowDraft()
|
|
}
|
|
|
|
saveStateToHistory(WorkflowHistoryEvent.NodeChange, {
|
|
nodeId: currentNodeId,
|
|
})
|
|
},
|
|
[
|
|
getNodesReadOnly,
|
|
store,
|
|
handleSyncWorkflowDraft,
|
|
saveStateToHistory,
|
|
nodesMetaDataMap,
|
|
autoGenerateWebhookUrl,
|
|
],
|
|
)
|
|
|
|
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
|
|
if (node.type === CUSTOM_NOTE_NODE)
|
|
return true
|
|
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
|
|
if (node.type === CUSTOM_NOTE_NODE)
|
|
return true
|
|
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: {
|
|
...(nodeToPaste.type !== CUSTOM_NOTE_NODE && 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])
|
|
|
|
// Check if there are any nodes selected via box selection
|
|
const hasBundledNodes = useCallback(() => {
|
|
const { getNodes } = store.getState()
|
|
const nodes = getNodes()
|
|
return nodes.some(node => node.data._isBundled)
|
|
}, [store])
|
|
|
|
const getCanMakeGroup = useCallback(() => {
|
|
const { getNodes, edges } = store.getState()
|
|
const nodes = getNodes()
|
|
const bundledNodes = nodes.filter(node => node.data._isBundled)
|
|
|
|
if (bundledNodes.length <= 1)
|
|
return false
|
|
|
|
const bundledNodeIds = bundledNodes.map(node => node.id)
|
|
const minimalEdges = edges.map(edge => ({
|
|
id: edge.id,
|
|
source: edge.source,
|
|
sourceHandle: edge.sourceHandle || 'source',
|
|
target: edge.target,
|
|
}))
|
|
const hasGroupNode = bundledNodes.some(node => node.data.type === BlockEnum.Group)
|
|
|
|
const { canMakeGroup } = checkMakeGroupAvailability(bundledNodeIds, minimalEdges, hasGroupNode)
|
|
return canMakeGroup
|
|
}, [store])
|
|
|
|
const handleMakeGroup = useCallback(() => {
|
|
const { getNodes, setNodes, edges, setEdges } = store.getState()
|
|
const nodes = getNodes()
|
|
const bundledNodes = nodes.filter(node => node.data._isBundled)
|
|
|
|
if (bundledNodes.length <= 1)
|
|
return
|
|
|
|
const bundledNodeIds = bundledNodes.map(node => node.id)
|
|
const minimalEdges = edges.map(edge => ({
|
|
id: edge.id,
|
|
source: edge.source,
|
|
sourceHandle: edge.sourceHandle || 'source',
|
|
target: edge.target,
|
|
}))
|
|
const hasGroupNode = bundledNodes.some(node => node.data.type === BlockEnum.Group)
|
|
|
|
const { canMakeGroup } = checkMakeGroupAvailability(bundledNodeIds, minimalEdges, hasGroupNode)
|
|
if (!canMakeGroup)
|
|
return
|
|
|
|
const bundledNodeIdSet = new Set(bundledNodeIds)
|
|
const bundledNodeIdIsLeaf = new Set<string>()
|
|
const inboundEdges = edges.filter(edge => !bundledNodeIdSet.has(edge.source) && bundledNodeIdSet.has(edge.target))
|
|
const outboundEdges = edges.filter(edge => bundledNodeIdSet.has(edge.source) && !bundledNodeIdSet.has(edge.target))
|
|
|
|
// leaf node: no outbound edges to other nodes in the selection
|
|
const handlers: GroupHandler[] = []
|
|
const leafNodeIdSet = new Set<string>()
|
|
|
|
bundledNodes.forEach((node: Node) => {
|
|
const targetBranches = node.data._targetBranches || [{ id: 'source', name: node.data.title }]
|
|
targetBranches.forEach((branch) => {
|
|
// A branch should be a handler if it's either:
|
|
// 1. Connected to a node OUTSIDE the group
|
|
// 2. NOT connected to any node INSIDE the group
|
|
const isConnectedInside = edges.some(edge =>
|
|
edge.source === node.id
|
|
&& (edge.sourceHandle === branch.id || (!edge.sourceHandle && branch.id === 'source'))
|
|
&& bundledNodeIdSet.has(edge.target),
|
|
)
|
|
const isConnectedOutside = edges.some(edge =>
|
|
edge.source === node.id
|
|
&& (edge.sourceHandle === branch.id || (!edge.sourceHandle && branch.id === 'source'))
|
|
&& !bundledNodeIdSet.has(edge.target),
|
|
)
|
|
|
|
if (isConnectedOutside || !isConnectedInside) {
|
|
const handlerId = `${node.id}-${branch.id}`
|
|
handlers.push({
|
|
id: handlerId,
|
|
label: branch.name || node.data.title || node.id,
|
|
nodeId: node.id,
|
|
sourceHandle: branch.id,
|
|
})
|
|
leafNodeIdSet.add(node.id)
|
|
}
|
|
})
|
|
})
|
|
|
|
const leafNodeIds = Array.from(leafNodeIdSet)
|
|
leafNodeIds.forEach(id => bundledNodeIdIsLeaf.add(id))
|
|
|
|
const members: GroupMember[] = bundledNodes.map((node) => {
|
|
return {
|
|
id: node.id,
|
|
type: node.data.type,
|
|
label: node.data.title,
|
|
}
|
|
})
|
|
|
|
// head nodes: nodes that receive input from outside the group
|
|
const headNodeIds = [...new Set(inboundEdges.map(edge => edge.target))]
|
|
|
|
// put the group node at the top-left corner of the selection, slightly offset
|
|
const { x: minX, y: minY } = getTopLeftNodePosition(bundledNodes)
|
|
|
|
const groupNodeData: GroupNodeData = {
|
|
title: t('operator.makeGroup', { ns: 'workflow' }),
|
|
desc: '',
|
|
type: BlockEnum.Group,
|
|
members,
|
|
handlers,
|
|
headNodeIds,
|
|
leafNodeIds,
|
|
selected: true,
|
|
_targetBranches: handlers.map(handler => ({
|
|
id: handler.id,
|
|
name: handler.label || handler.id,
|
|
})),
|
|
}
|
|
|
|
const { newNode: groupNode } = generateNewNode({
|
|
data: groupNodeData,
|
|
position: {
|
|
x: minX - 20,
|
|
y: minY - 20,
|
|
},
|
|
})
|
|
|
|
const nodeTypeMap = new Map(nodes.map(node => [node.id, node.data.type]))
|
|
|
|
const newNodes = produce(nodes, (draft) => {
|
|
draft.forEach((node) => {
|
|
if (bundledNodeIdSet.has(node.id)) {
|
|
node.data._isBundled = false
|
|
node.selected = false
|
|
node.hidden = true
|
|
node.data._hiddenInGroupId = groupNode.id
|
|
}
|
|
else {
|
|
node.data._isBundled = false
|
|
}
|
|
})
|
|
draft.push(groupNode)
|
|
})
|
|
|
|
const newEdges = produce(edges, (draft) => {
|
|
draft.forEach((edge) => {
|
|
if (bundledNodeIdSet.has(edge.source) || bundledNodeIdSet.has(edge.target)) {
|
|
edge.hidden = true
|
|
edge.data = {
|
|
...edge.data,
|
|
_hiddenInGroupId: groupNode.id,
|
|
_isBundled: false,
|
|
}
|
|
}
|
|
else if (edge.data?._isBundled) {
|
|
edge.data._isBundled = false
|
|
}
|
|
})
|
|
|
|
// re-add the external inbound edges to the group node as UI-only edges (not persisted to backend)
|
|
inboundEdges.forEach((edge) => {
|
|
draft.push({
|
|
id: `${edge.id}__to-${groupNode.id}`,
|
|
type: edge.type || CUSTOM_EDGE,
|
|
source: edge.source,
|
|
target: groupNode.id,
|
|
sourceHandle: edge.sourceHandle,
|
|
targetHandle: 'target',
|
|
data: {
|
|
...edge.data,
|
|
sourceType: nodeTypeMap.get(edge.source)!,
|
|
targetType: BlockEnum.Group,
|
|
_hiddenInGroupId: undefined,
|
|
_isBundled: false,
|
|
_isTemp: true, // UI-only edge, not persisted to backend
|
|
},
|
|
zIndex: edge.zIndex,
|
|
})
|
|
})
|
|
|
|
// outbound edges of the group node as UI-only edges (not persisted to backend)
|
|
outboundEdges.forEach((edge) => {
|
|
if (!bundledNodeIdIsLeaf.has(edge.source))
|
|
return
|
|
|
|
// Use the same handler id format: nodeId-sourceHandle
|
|
const originalSourceHandle = edge.sourceHandle || 'source'
|
|
const handlerId = `${edge.source}-${originalSourceHandle}`
|
|
|
|
draft.push({
|
|
id: `${groupNode.id}-${edge.target}-${edge.targetHandle || 'target'}-${handlerId}`,
|
|
type: edge.type || CUSTOM_EDGE,
|
|
source: groupNode.id,
|
|
target: edge.target,
|
|
sourceHandle: handlerId,
|
|
targetHandle: edge.targetHandle,
|
|
data: {
|
|
...edge.data,
|
|
sourceType: BlockEnum.Group,
|
|
targetType: nodeTypeMap.get(edge.target)!,
|
|
_hiddenInGroupId: undefined,
|
|
_isBundled: false,
|
|
_isTemp: true,
|
|
},
|
|
zIndex: edge.zIndex,
|
|
})
|
|
})
|
|
})
|
|
|
|
setNodes(newNodes)
|
|
setEdges(newEdges)
|
|
workflowStore.setState({
|
|
selectionMenu: undefined,
|
|
})
|
|
handleSyncWorkflowDraft()
|
|
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, {
|
|
nodeId: groupNode.id,
|
|
})
|
|
}, [handleSyncWorkflowDraft, saveStateToHistory, store, t, workflowStore])
|
|
|
|
// check if the current selection can be ungrouped (single selected Group node)
|
|
const getCanUngroup = useCallback(() => {
|
|
const { getNodes } = store.getState()
|
|
const nodes = getNodes()
|
|
const selectedNodes = nodes.filter(node => node.selected)
|
|
|
|
if (selectedNodes.length !== 1)
|
|
return false
|
|
|
|
return selectedNodes[0].data.type === BlockEnum.Group
|
|
}, [store])
|
|
|
|
// get the selected group node id for ungroup operation
|
|
const getSelectedGroupId = useCallback(() => {
|
|
const { getNodes } = store.getState()
|
|
const nodes = getNodes()
|
|
const selectedNodes = nodes.filter(node => node.selected)
|
|
|
|
if (selectedNodes.length === 1 && selectedNodes[0].data.type === BlockEnum.Group)
|
|
return selectedNodes[0].id
|
|
|
|
return undefined
|
|
}, [store])
|
|
|
|
const handleUngroup = useCallback((groupId: string) => {
|
|
const { getNodes, setNodes, edges, setEdges } = store.getState()
|
|
const nodes = getNodes()
|
|
const groupNode = nodes.find(n => n.id === groupId)
|
|
|
|
if (!groupNode || groupNode.data.type !== BlockEnum.Group)
|
|
return
|
|
|
|
const memberIds = new Set((groupNode.data.members || []).map((m: { id: string }) => m.id))
|
|
|
|
// restore hidden member nodes
|
|
const newNodes = produce(nodes, (draft) => {
|
|
draft.forEach((node) => {
|
|
if (memberIds.has(node.id)) {
|
|
node.hidden = false
|
|
delete node.data._hiddenInGroupId
|
|
}
|
|
})
|
|
// remove group node
|
|
const groupIndex = draft.findIndex(n => n.id === groupId)
|
|
if (groupIndex !== -1)
|
|
draft.splice(groupIndex, 1)
|
|
})
|
|
|
|
// restore hidden edges and remove temp edges in single pass O(E)
|
|
const newEdges = produce(edges, (draft) => {
|
|
const indicesToRemove: number[] = []
|
|
|
|
for (let i = 0; i < draft.length; i++) {
|
|
const edge = draft[i]
|
|
// restore hidden edges that involve member nodes
|
|
if (edge.hidden && (memberIds.has(edge.source) || memberIds.has(edge.target)))
|
|
edge.hidden = false
|
|
// collect temp edges connected to group for removal
|
|
if (edge.data?._isTemp && (edge.source === groupId || edge.target === groupId))
|
|
indicesToRemove.push(i)
|
|
}
|
|
|
|
// remove collected indices in reverse order to avoid index shift
|
|
for (let i = indicesToRemove.length - 1; i >= 0; i--)
|
|
draft.splice(indicesToRemove[i], 1)
|
|
})
|
|
|
|
setNodes(newNodes)
|
|
setEdges(newEdges)
|
|
handleSyncWorkflowDraft()
|
|
saveStateToHistory(WorkflowHistoryEvent.NodeDelete, {
|
|
nodeId: groupId,
|
|
})
|
|
}, [handleSyncWorkflowDraft, saveStateToHistory, store])
|
|
|
|
return {
|
|
handleNodeDragStart,
|
|
handleNodeDrag,
|
|
handleNodeDragStop,
|
|
handleNodeEnter,
|
|
handleNodeLeave,
|
|
handleNodeSelect,
|
|
handleNodeClick,
|
|
handleNodeConnect,
|
|
handleNodeConnectStart,
|
|
handleNodeConnectEnd,
|
|
handleNodeDelete,
|
|
handleNodeChange,
|
|
handleNodeAdd,
|
|
handleNodesCancelSelected,
|
|
handleNodeContextMenu,
|
|
handleNodesCopy,
|
|
handleNodesPaste,
|
|
handleNodesDuplicate,
|
|
handleNodesDelete,
|
|
handleMakeGroup,
|
|
handleUngroup,
|
|
handleNodeResize,
|
|
handleNodeDisconnect,
|
|
handleHistoryBack,
|
|
handleHistoryForward,
|
|
dimOtherNodes,
|
|
undimAllNodes,
|
|
hasBundledNodes,
|
|
getCanMakeGroup,
|
|
getCanUngroup,
|
|
getSelectedGroupId,
|
|
}
|
|
}
|