import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; import type { NodeConfig } from './types'; import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types'; import { colorForType, isCompoundType } from './node-colors'; import { DiagramNode } from './DiagramNode'; import { DiagramEdge } from './DiagramEdge'; import styles from './ProcessDiagram.module.css'; const HEADER_HEIGHT = 22; const CORNER_RADIUS = 4; interface CompoundNodeProps { node: DiagramNodeType; /** All edges for this section — compound filters to its own internal edges */ edges: DiagramEdgeType[]; /** Absolute offset of the nearest compound ancestor (for coordinate adjustment) */ parentX?: number; parentY?: number; selectedNodeId?: string; hoveredNodeId: string | null; nodeConfigs?: Map; /** Execution overlay for edge traversal coloring */ executionOverlay?: Map; /** Whether an execution overlay is active (enables dimming of skipped nodes) */ overlayActive?: boolean; /** Per-compound iteration state */ iterationState?: Map; /** Called when user changes iteration on a compound stepper */ onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void; onNodeClick: (nodeId: string) => void; onNodeDoubleClick?: (nodeId: string) => void; onNodeEnter: (nodeId: string) => void; onNodeLeave: () => void; } export function CompoundNode({ node, edges, parentX = 0, parentY = 0, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, overlayActive, iterationState, onIterationChange, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }: CompoundNodeProps) { const x = (node.x ?? 0) - parentX; const y = (node.y ?? 0) - parentY; const absX = node.x ?? 0; const absY = node.y ?? 0; const w = node.width ?? 200; const h = node.height ?? 100; const color = colorForType(node.type); const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; const label = node.label ? `${typeName}: ${node.label}` : typeName; const iterationInfo = node.id ? iterationState?.get(node.id) : undefined; const headerWidth = w; // Collect all descendant node IDs to filter edges that belong inside this compound const descendantIds = new Set(); collectIds(node.children ?? [], descendantIds); const internalEdges = edges.filter( e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId), ); const childProps = { edges, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, overlayActive, iterationState, onIterationChange, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }; // _TRY_BODY: transparent wrapper — no header, no border, just layout if (node.type === '_TRY_BODY') { return ( {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)} ); } // DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label) if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') { const sectionLabel = node.type === 'DO_CATCH' ? (node.label ? `catch: ${node.label}` : 'catch') : (node.label ? `finally: ${node.label}` : 'finally'); return ( {/* Tinted background */} {/* Border */} {/* Section label */} {sectionLabel} {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)} ); } // Default compound rendering (DO_TRY, EIP_CHOICE, etc.) return ( {/* Container body */} {/* Colored header bar */} {/* Header label */} {label} {/* Iteration stepper (for LOOP, SPLIT, MULTICAST with overlay data) */} {iterationInfo && (
{iterationInfo.current + 1} / {iterationInfo.total}
)} {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)}
); } /** Render internal edges adjusted for compound coordinates */ function renderInternalEdges( internalEdges: DiagramEdgeType[], absX: number, absY: number, executionOverlay?: Map, ) { return ( {internalEdges.map((edge, i) => { const isTraversed = executionOverlay ? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId)) : undefined; return ( [p[0] - absX, p[1] - absY]), }} traversed={isTraversed} /> ); })} ); } /** Render children — compounds recurse, leaves become DiagramNode */ function renderChildren( node: DiagramNodeType, absX: number, absY: number, props: Omit, ) { return ( <> {node.children?.map(child => { if (isCompoundType(child.type) && child.children && child.children.length > 0) { return ( ); } return ( child.id && props.onNodeClick(child.id)} onDoubleClick={() => child.id && props.onNodeDoubleClick?.(child.id)} onMouseEnter={() => child.id && props.onNodeEnter(child.id)} onMouseLeave={props.onNodeLeave} /> ); })} ); } function collectIds(nodes: DiagramNodeType[], set: Set) { for (const n of nodes) { if (n.id) set.add(n.id); if (n.children) collectIds(n.children, set); } }