'use client'; import * as React from 'react'; import { ReactFlow, ReactFlowProvider, Background, Controls, type NodeTypes, type EdgeTypes, type Connection, } from '@xyflow/react'; import { cn } from '../../lib/utils'; import { WorkflowNode } from './WorkflowNode'; import { WorkflowEdge } from './WorkflowEdge'; import { useCanvasGraph } from './hooks/useCanvasGraph'; import { useNodeStatusOverlay } from './hooks/useNodeStatusOverlay'; import { connectionToEdge } from './graph-ops'; import type { WorkflowGraph, WfEdge, WfExecutionView, NodeTypeDef } from './types'; import type { LayoutDirection } from './layout/auto-layout'; const nodeTypes: NodeTypes = { workflowNode: WorkflowNode }; const edgeTypes: EdgeTypes = { workflowEdge: WorkflowEdge }; export interface WorkflowCanvasProps { /** The graph to render. */ graph: WorkflowGraph; /** Node-type catalog (enriches nodes with category + ports). */ nodeTypes?: NodeTypeDef[]; /** * Optional run overlay. When present, node status badges and edge * animations reflect the live execution. Pass a fresh snapshot per poll. */ execution?: WfExecutionView; /** Layout direction for positionless nodes. Default 'LR'. */ direction?: LayoutDirection; /** Inline-rename callback. */ onRename?: (nodeId: string, name: string) => void; /** Fired when a node is selected on the canvas. */ onSelectNode?: (nodeId: string | null) => void; /** Fired when an edge is selected on the canvas. */ onSelectEdge?: (edgeId: string | null) => void; /** * Currently-selected node id. Threaded onto xyflow's `selected` flag so the * controlled canvas knows what's selected — required for keyboard delete. */ selectedNodeId?: string | null; /** Currently-selected edge id (same reason as {@link selectedNodeId}). */ selectedEdgeId?: string | null; /** Drop handler for palette drag-and-drop (slug + client coords). */ onDropNodeType?: (slug: string, clientX: number, clientY: number) => void; /** Fired when the user draws a connection between two node handles. */ onConnectEdge?: (edge: WfEdge) => void; /** Fired with the ids of nodes removed via select+Delete/Backspace. */ onDeleteNodes?: (nodeIds: string[]) => void; /** Fired with the edges removed via select+Delete/Backspace. */ onDeleteEdges?: (edges: WfEdge[]) => void; className?: string; } function CanvasInner({ graph, nodeTypes: catalog, execution, direction = 'LR', onRename, onSelectNode, onSelectEdge, selectedNodeId, selectedEdgeId, onDropNodeType, onConnectEdge, onDeleteNodes, onDeleteEdges, className, }: WorkflowCanvasProps) { const { nodes: baseNodes, edges: baseEdges } = useCanvasGraph(graph, { nodeTypes: catalog, direction, onRename, }); const { nodes: overlaidNodes, edges: overlaidEdges } = useNodeStatusOverlay( baseNodes, baseEdges, execution ); // Thread our selection into xyflow's `selected` flag. The canvas is fully // controlled (no onNodesChange), so without this xyflow never marks a // clicked node selected and keyboard delete has nothing to remove. const nodes = React.useMemo( () => selectedNodeId == null ? overlaidNodes : overlaidNodes.map((n) => n.id === selectedNodeId ? { ...n, selected: true } : n ), [overlaidNodes, selectedNodeId] ); const edges = React.useMemo( () => selectedEdgeId == null ? overlaidEdges : overlaidEdges.map((e) => e.id === selectedEdgeId ? { ...e, selected: true } : e ), [overlaidEdges, selectedEdgeId] ); const handleDragOver = React.useCallback((e: React.DragEvent) => { if (!onDropNodeType) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }, [onDropNodeType]); const handleDrop = React.useCallback( (e: React.DragEvent) => { if (!onDropNodeType) return; e.preventDefault(); const slug = e.dataTransfer.getData('application/x-workflow-node'); if (slug) onDropNodeType(slug, e.clientX, e.clientY); }, [onDropNodeType] ); const handleConnect = React.useCallback( (conn: Connection) => { if (!onConnectEdge || !conn.source || !conn.target) return; onConnectEdge(connectionToEdge(conn)); }, [onConnectEdge] ); // Only enable keyboard delete when a delete handler is wired. const deletable = !!onDeleteNodes || !!onDeleteEdges; return (
onDeleteNodes(removed.map((n) => n.id)) : undefined } onEdgesDelete={ onDeleteEdges ? (removed) => onDeleteEdges( removed.map((e) => connectionToEdge({ source: e.source, target: e.target, sourceHandle: e.sourceHandle, targetHandle: e.targetHandle, }) ) ) : undefined } onNodeClick={(_, n) => { onSelectNode?.(n.id); onSelectEdge?.(null); }} onEdgeClick={(_, e) => { onSelectEdge?.(e.id); onSelectNode?.(null); }} onPaneClick={() => { onSelectNode?.(null); onSelectEdge?.(null); }} >
); } /** * Renders a {@link WorkflowGraph} on an interactive xyflow canvas with custom * workflow nodes/edges, auto-layout, fit-view, and controls. When an * `execution` overlay is supplied, node statuses + edge animations reflect the * live run. Wraps its own {@link ReactFlowProvider} so it can be dropped in * anywhere. */ export function WorkflowCanvas(props: WorkflowCanvasProps) { return ( ); }