Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | 3x 3x 3x 3x 3x 3x 6x 6x 3x 3x 3x 3x 5x 3x 3x 5x 3x 5x 3x 3x 5x 5x 3x 3x 5x 5x 5x 3x | import { useMemo } from "react";
import dagre from "@dagrejs/dagre";
import type { Node, Edge } from "@xyflow/react";
import type { TaskData } from "../../hooks/types.js";
/** Width of each task node in the DAG layout (pixels). */
const NODE_WIDTH: number = 220;
/** Height of each task node in the DAG layout (pixels). */
const NODE_HEIGHT: number = 70;
/** Horizontal separation between sibling nodes (pixels). */
const NODE_SEPARATION: number = 40;
/** Vertical separation between rank levels (pixels). */
const RANK_SEPARATION: number = 60;
/** Edge type identifier for parent→child (hierarchy) edges. */
const EDGE_TYPE_HIERARCHY: string = "hierarchy";
/** Edge type identifier for dependency edges. */
const EDGE_TYPE_DEPENDENCY: string = "dependency";
/** Data attached to each React Flow task node. */
export interface TaskNodeData extends Record<string, unknown> {
task: TaskData;
childCount: number;
doneChildCount: number;
hasDependencies: boolean;
}
/** Result of the DAG layout computation. */
export interface DagLayoutResult {
nodes: Node<TaskNodeData>[];
edges: Edge[];
}
/**
* Computes a dagre-based DAG layout from a flat list of tasks.
* Produces positioned React Flow nodes and edges for both hierarchy
* (parent→child) and dependency relationships.
*/
export function useDagLayout(tasks: TaskData[]): DagLayoutResult {
return useMemo(() => {
if (tasks.length === 0) {
return { nodes: [], edges: [] };
}
// Enable multigraph so hierarchy and dependency edges between the same
// pair of nodes are both preserved in the layout graph.
const graph = new dagre.graphlib.Graph({ multigraph: true });
graph.setDefaultEdgeLabel(() => ({}));
graph.setGraph({
rankdir: "TB",
nodesep: NODE_SEPARATION,
ranksep: RANK_SEPARATION,
});
const taskById = new Map(tasks.map((t) => [t.id, t]));
// Precompute children per parent to avoid O(n^2) lookups when building nodes.
const childrenByParent = new Map<string, TaskData[]>();
for (const task of tasks) {
Iif (task.parentTaskId && taskById.has(task.parentTaskId)) {
const siblings = childrenByParent.get(task.parentTaskId) || [];
siblings.push(task);
childrenByParent.set(task.parentTaskId, siblings);
}
}
// Add nodes
for (const task of tasks) {
graph.setNode(task.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
}
// Add edges
const edges: Edge[] = [];
for (const task of tasks) {
// Parent → child edges
Iif (task.parentTaskId && taskById.has(task.parentTaskId)) {
const edgeId = `hierarchy-${task.parentTaskId}-${task.id}`;
graph.setEdge(task.parentTaskId, task.id, {}, edgeId);
edges.push({
id: edgeId,
source: task.parentTaskId,
target: task.id,
type: "smoothstep",
data: { edgeType: EDGE_TYPE_HIERARCHY },
style: { stroke: "var(--accent-green)", strokeWidth: 2 },
animated: false,
});
}
// Dependency edges
for (const depId of task.dependsOn) {
Iif (taskById.has(depId)) {
const edgeId = `dependency-${depId}-${task.id}`;
graph.setEdge(depId, task.id, {}, edgeId);
edges.push({
id: edgeId,
source: depId,
target: task.id,
type: "smoothstep",
data: { edgeType: EDGE_TYPE_DEPENDENCY },
style: {
stroke: "var(--text-tertiary)",
strokeWidth: 1.5,
strokeDasharray: "6 3",
},
animated: false,
});
}
}
}
// Run dagre layout
dagre.layout(graph);
// Map dagre positions to React Flow nodes
const nodes: Node<TaskNodeData>[] = tasks.map((task) => {
const nodeWithPosition = graph.node(task.id) as { x: number; y: number };
const children = childrenByParent.get(task.id) || [];
return {
id: task.id,
type: "task",
position: {
x: nodeWithPosition.x - NODE_WIDTH / 2,
y: nodeWithPosition.y - NODE_HEIGHT / 2,
},
data: {
task,
childCount: children.length,
doneChildCount: children.filter((c) => c.status === "complete").length,
hasDependencies: task.dependsOn.length > 0,
},
};
});
return { nodes, edges };
}, [tasks]);
}
|