import React, { useEffect, useRef, useMemo, useCallback, useState } from "react"; import type { Task } from "../../types"; import type { GraphFilters } from "../types/graph"; import { buildDependencyGraph, generateMermaidFlowchart, filterGraph, extractTaskIdFromNodeId, } from "../utils/dependency-graph"; import { ensureMermaid } from "../utils/mermaid"; interface DependencyViewProps { tasks: Task[]; onEditTask: (task: Task) => void; filters?: GraphFilters; } export default function DependencyView({ tasks, onEditTask, filters }: DependencyViewProps) { const containerRef = useRef(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); // Create task lookup map const taskMap = useMemo(() => new Map(tasks.map((t) => [t.id, t])), [tasks]); // Build and filter graph, generate Mermaid code const { mermaidCode, nodeCount, edgeCount } = useMemo(() => { const fullGraph = buildDependencyGraph(tasks); const graph = filters ? filterGraph(fullGraph, filters) : fullGraph; return { mermaidCode: generateMermaidFlowchart(graph), nodeCount: graph.nodes.length, edgeCount: graph.edges.length, }; }, [tasks, filters]); // Attach click handlers to rendered SVG nodes const attachClickHandlers = useCallback( (svg: SVGElement) => { // Find all node groups (Mermaid uses .node class) const nodes = svg.querySelectorAll(".node"); for (const node of nodes) { const nodeEl = node as SVGElement; const nodeId = nodeEl.getAttribute("id") || ""; const taskId = extractTaskIdFromNodeId(nodeId); if (taskId) { nodeEl.style.cursor = "pointer"; // Click to open task details nodeEl.addEventListener("click", () => { const task = taskMap.get(taskId); if (task) onEditTask(task); }); // Hover effects for dependency highlighting nodeEl.addEventListener("mouseenter", () => { highlightDependencyPath(svg, taskId, taskMap); }); nodeEl.addEventListener("mouseleave", () => { clearHighlights(svg); }); } } }, [taskMap, onEditTask], ); // Render Mermaid diagram useEffect(() => { if (!containerRef.current) return; const render = async () => { setIsLoading(true); setError(null); try { const m = await ensureMermaid(); // Initialize with loose security to allow click handlers m.default.initialize({ startOnLoad: false, securityLevel: "loose", theme: "default", }); // Generate unique ID for this render const id = `dependency-graph-${Date.now()}`; // Render the diagram const result = await m.default.render(id, mermaidCode); if (containerRef.current) { containerRef.current.innerHTML = result.svg; // Bind any Mermaid-provided functions if (result.bindFunctions && containerRef.current) { result.bindFunctions(containerRef.current); } // Attach our custom click handlers const svgElement = containerRef.current.querySelector("svg"); if (svgElement) { attachClickHandlers(svgElement); } } } catch (err) { console.error("Failed to render dependency graph:", err); setError(err instanceof Error ? err.message : "Failed to render graph"); } finally { setIsLoading(false); } }; render(); }, [mermaidCode, attachClickHandlers]); // Empty state if (tasks.length === 0) { return (

No tasks to display

Create some tasks to see the dependency graph

); } // No nodes after filtering if (nodeCount === 0) { return (

No tasks match filters

Try adjusting your filter criteria

); } return (
{/* Stats bar */}
{nodeCount} task{nodeCount !== 1 ? "s" : ""} • {edgeCount} dependenc{edgeCount !== 1 ? "ies" : "y"} Click a task to view details • Hover to highlight dependencies
{/* Loading overlay */} {isLoading && (
Loading graph...
)} {/* Error state */} {error && (

Failed to render graph

{error}

)} {/* Graph container */}
{/* Legend */}
Legend:
Done
In Progress
To Do
High Priority
); } /** * Highlight the dependency path for a task (its dependencies and dependents) */ function highlightDependencyPath(svg: SVGElement, taskId: string, taskMap: Map): void { const task = taskMap.get(taskId); if (!task) return; // Build set of connected task IDs const connectedIds = new Set([taskId]); for (const depId of task.dependencies || []) { connectedIds.add(depId); } // Find tasks that depend on this one for (const [id, t] of taskMap) { if (t.dependencies?.includes(taskId)) { connectedIds.add(id); } } // Dim non-connected nodes const allNodes = svg.querySelectorAll(".node"); for (const node of allNodes) { const nodeEl = node as SVGElement; const nodeId = nodeEl.getAttribute("id") || ""; const nodeTaskId = extractTaskIdFromNodeId(nodeId); if (nodeTaskId && !connectedIds.has(nodeTaskId)) { nodeEl.style.opacity = "0.3"; } } // Dim non-connected edges const allEdges = svg.querySelectorAll(".edgePath"); for (const edge of allEdges) { const edgeEl = edge as SVGElement; const edgeId = edgeEl.getAttribute("id") || ""; // Check if edge connects to our task let isConnected = false; for (const connectedId of connectedIds) { if (edgeId.includes(connectedId)) { isConnected = true; break; } } if (!isConnected) { edgeEl.style.opacity = "0.1"; } else { // Highlight connected edges const path = edgeEl.querySelector("path"); if (path) { path.style.stroke = "#3b82f6"; path.style.strokeWidth = "3"; } } } } /** * Clear all highlight effects */ function clearHighlights(svg: SVGElement): void { // Reset node opacity const allNodes = svg.querySelectorAll(".node"); for (const node of allNodes) { (node as SVGElement).style.opacity = ""; } // Reset edge opacity and styles const allEdges = svg.querySelectorAll(".edgePath"); for (const edge of allEdges) { const edgeEl = edge as SVGElement; edgeEl.style.opacity = ""; const path = edgeEl.querySelector("path"); if (path) { path.style.stroke = ""; path.style.strokeWidth = ""; } } }