"use client"; import "@xyflow/react/dist/style.css"; import { Background, BackgroundVariant, Controls, MiniMap, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, type Edge, } from "@xyflow/react"; import { useEffect, useMemo } from "react"; import { collectVisible, layoutWithDagre } from "./graphLayout"; import { OrgChartNode, type OrgChartRFNode } from "./OrgChartNode"; import type { ChartNode, ChartNodeKind } from "./types"; // Referentially stable across renders — React Flow treats a new nodeTypes // object as a change and remounts every custom node otherwise. const NODE_TYPES = { orgNode: OrgChartNode }; const MINIMAP_COLORS: Record = { person: "#d6046e", role: "#5c2e91", group: "#c9b3c0", vacancy: "#f6cfe2", }; export type GraphOrgChartProps = { tree: ChartNode[]; isExpanded: (id: string) => boolean; onToggle: (id: string) => void; matchedIds?: Set | null; }; export function GraphOrgChart(props: GraphOrgChartProps) { return ( ); } function GraphOrgChartInner({ tree, isExpanded, onToggle, matchedIds }: GraphOrgChartProps) { const { visibleNodes, visibleEdges } = useMemo(() => collectVisible(tree, isExpanded), [tree, isExpanded]); const { rfNodes, rfEdges } = useMemo(() => { const positions = layoutWithDagre(visibleNodes, visibleEdges); const rfNodes: OrgChartRFNode[] = visibleNodes.map((n) => ({ id: n.id, type: "orgNode", position: positions.get(n.id) ?? { x: 0, y: 0 }, draggable: false, data: { chartNode: n, expanded: n.children.length > 0 && isExpanded(n.id), hasChildren: n.children.length > 0, childCount: n.children.length, onToggle, }, })); const rfEdges: Edge[] = visibleEdges.map((e) => ({ id: e.id, source: e.source, target: e.target, type: "smoothstep", pathOptions: { borderRadius: 14 }, style: { stroke: "#e3cddb", strokeWidth: 1.5 }, })); return { rfNodes, rfEdges }; }, [visibleNodes, visibleEdges, isExpanded, onToggle]); const [nodes, setNodes, onNodesChange] = useNodesState(rfNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(rfEdges); const { fitView } = useReactFlow(); useEffect(() => { setNodes(rfNodes); setEdges(rfEdges); const raf = requestAnimationFrame(() => fitView({ padding: 0.2, duration: 300 })); return () => cancelAnimationFrame(raf); }, [rfNodes, rfEdges, setNodes, setEdges, fitView]); useEffect(() => { if (!matchedIds || matchedIds.size === 0) return; const t = setTimeout(() => { const ids = [...matchedIds].filter((id) => visibleNodes.some((n) => n.id === id)); if (ids.length > 0) fitView({ nodes: ids.map((id) => ({ id })), padding: 0.3, duration: 400 }); }, 150); return () => clearTimeout(t); }, [matchedIds, visibleNodes, fitView]); return ( // dvh, not vh: on iOS/Android the browser chrome collapses on scroll, and // vh is measured against the *expanded* viewport — the canvas would hang // off the bottom of the screen for as long as the toolbar is showing.
{/* Hidden under lg via CSS: on a phone it would cover a real fraction of the canvas for little navigational benefit. */} MINIMAP_COLORS[(n.data as OrgChartNodeDataLike).chartNode.kind] ?? "#d6046e"} nodeStrokeWidth={0} nodeBorderRadius={3} />
); } type OrgChartNodeDataLike = { chartNode: { kind: ChartNodeKind } };