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), ); return ( {/* Container body */} {/* Colored header bar */} {/* Header label */} {label} {/* Iteration stepper (for LOOP, SPLIT, MULTICAST with overlay data) */} {iterationInfo && (
{iterationInfo.current + 1} / {iterationInfo.total}
)} {/* Internal edges (rendered after background, before children) */} {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} /> ); })} {/* Children — recurse into compound children, render leaves as DiagramNode */} {node.children?.map(child => { if (isCompoundType(child.type) && child.children && child.children.length > 0) { return ( ); } return ( child.id && onNodeClick(child.id)} onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)} onMouseEnter={() => child.id && onNodeEnter(child.id)} onMouseLeave={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); } }