feat(elk-layout): add support for Human Input nodes with ELK ports and enhance edge sorting

This commit is contained in:
twwu 2026-01-12 16:00:48 +08:00
parent 7deaab116a
commit 23f5427349
1 changed files with 87 additions and 2 deletions

View File

@ -1,4 +1,5 @@
import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
import type {
Edge,
@ -345,6 +346,76 @@ const buildIfElseWithPorts = (
}
}
/**
* Build Human Input node with ELK native Ports for multiple branches
* Handles user actions as branches with __timeout as the last fixed branch
*/
const buildHumanInputWithPorts = (
humanInputNode: Node,
edges: Edge[],
): { node: ElkNodeShape, portMap: Map<string, string> } | null => {
const childEdges = edges.filter(edge => edge.source === humanInputNode.id)
if (childEdges.length <= 1)
return null
// Sort child edges: user actions first (by order), then __timeout last
const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const userActions = (humanInputNode.data as HumanInputNodeType).user_actions || []
const isATimeout = handleA === '__timeout'
const isBTimeout = handleB === '__timeout'
// __timeout should always be last
if (isATimeout)
return 1
if (isBTimeout)
return -1
// Sort by user_actions order
const indexA = userActions.findIndex(action => action.id === handleA)
const indexB = userActions.findIndex(action => action.id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
// Create ELK ports for each branch
const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({
id: `${humanInputNode.id}-port-${edge.sourceHandle || index}`,
layoutOptions: {
'port.side': 'EAST',
'port.index': String(index),
},
}))
// Build port mapping: edge.id -> portId
const portMap = new Map<string, string>()
sortedChildEdges.forEach((edge, index) => {
const portId = `${humanInputNode.id}-port-${edge.sourceHandle || index}`
portMap.set(edge.id, portId)
})
return {
node: {
id: humanInputNode.id,
width: humanInputNode.width ?? DEFAULT_NODE_WIDTH,
height: humanInputNode.height ?? DEFAULT_NODE_HEIGHT,
ports,
layoutOptions: {
'elk.portConstraints': 'FIXED_ORDER',
},
},
portMap,
}
}
const normaliseBounds = (layout: LayoutResult): LayoutResult => {
const {
nodes,
@ -388,8 +459,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
// Track which edges have been processed for If/Else nodes with ports
const edgeToPortMap = new Map<string, string>()
// Build nodes with ports for If/Else nodes
// todo: add human-input node support
// Build nodes with ports for If/Else and Human Input nodes
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)
@ -406,6 +476,21 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
elkNodes.push(toElkNode(node))
}
}
else if (node.data.type === BlockEnum.HumanInput) {
const portsResult = buildHumanInputWithPorts(node, edges)
if (portsResult) {
// Use node with ports
elkNodes.push(portsResult.node)
// Store port mappings for edges
portsResult.portMap.forEach((portId, edgeId) => {
edgeToPortMap.set(edgeId, portId)
})
}
else {
// No multiple branches, use normal node
elkNodes.push(toElkNode(node))
}
}
else {
elkNodes.push(toElkNode(node))
}