import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; import type { NodeConfig } from './types'; import type { NodeExecutionState } from '../ExecutionDiagram/types'; import { colorForType, isCompoundType } from './node-colors'; import { DiagramNode } from './DiagramNode'; import { DiagramEdge } from './DiagramEdge'; 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; 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, 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; // 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} {/* 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); } }