@parhelia/core 0.1.12436 → 0.1.12447
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/types.d.ts +4 -0
- package/dist/config/types.js.map +1 -1
- package/dist/editor/ContentTree.js +19 -7
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +59 -1
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/services/serviceHelper.js +5 -2
- package/dist/editor/services/serviceHelper.js.map +1 -1
- package/dist/editor/settings/panels/PersistentLogsPanel.js +25 -2
- package/dist/editor/settings/panels/PersistentLogsPanel.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +7 -1
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +3 -3
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js +143 -84
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/task-board/TaskBoardWorkspace.js +1 -1
- package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
- package/dist/task-board/assigneeDisplay.js +1 -3
- package/dist/task-board/assigneeDisplay.js.map +1 -1
- package/dist/task-board/components/CreateProjectDialog.js +2 -1
- package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
- package/dist/task-board/components/TaskboardPersistentLogPanel.js +32 -5
- package/dist/task-board/components/TaskboardPersistentLogPanel.js.map +1 -1
- package/dist/task-board/persistentLogCopy.d.ts +7 -0
- package/dist/task-board/persistentLogCopy.js +80 -0
- package/dist/task-board/persistentLogCopy.js.map +1 -0
- package/dist/task-board/types.d.ts +3 -0
- package/dist/task-board/utils/taskDependencyOrdering.d.ts +6 -0
- package/dist/task-board/utils/taskDependencyOrdering.js +138 -1
- package/dist/task-board/utils/taskDependencyOrdering.js.map +1 -1
- package/dist/task-board/views/DependencyGraphView.d.ts +5 -2
- package/dist/task-board/views/DependencyGraphView.js +178 -61
- package/dist/task-board/views/DependencyGraphView.js.map +1 -1
- package/dist/task-board/views/WizardView.js +26 -24
- package/dist/task-board/views/WizardView.js.map +1 -1
- package/dist/tour/Tour.js +63 -0
- package/dist/tour/Tour.js.map +1 -1
- package/dist/tour/default-tour.js +7 -0
- package/dist/tour/default-tour.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { ReactFlow, Controls, MiniMap, useNodesState, useEdgesState, Handle, Position, MarkerType, } from "@xyflow/react";
|
|
4
4
|
import Dagre from "@dagrejs/dagre";
|
|
5
5
|
import "@xyflow/react/dist/style.css";
|
|
@@ -10,7 +10,7 @@ import { cn } from "../../lib/utils";
|
|
|
10
10
|
import { saveGraphLayout } from "../services/taskService";
|
|
11
11
|
import { Button } from "../../components/ui/button";
|
|
12
12
|
import { Badge } from "../../components/ui/badge";
|
|
13
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip";
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
|
|
14
14
|
import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, Loader2, Plus, Trash2, } from "lucide-react";
|
|
15
15
|
const NODE_WIDTH = 220;
|
|
16
16
|
const NODE_HEIGHT = 80;
|
|
@@ -46,24 +46,37 @@ function TaskGraphNode({ data }) {
|
|
|
46
46
|
const priorityCfg = data.priority ? PRIORITY_CONFIG[data.priority] : null;
|
|
47
47
|
const PriorityIcon = priorityCfg?.icon;
|
|
48
48
|
const actionButtonClass = "nodrag nopan absolute z-20 flex h-5 w-5 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 shadow-sm transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600";
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const dependentButtonClass = data.dependencySourceHandle === "right"
|
|
50
|
+
? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
|
|
51
|
+
: "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
|
|
52
|
+
const dependentTooltipSide = data.dependencySourceHandle === "right" ? "right" : "bottom";
|
|
53
|
+
const subtaskButtonClass = data.hierarchySourceHandle === "right"
|
|
54
|
+
? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
|
|
55
|
+
: "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
|
|
56
|
+
const subtaskTooltipSide = data.hierarchySourceHandle === "right" ? "right" : "bottom";
|
|
57
|
+
const nodeSurfaceClass = data.isSelected
|
|
58
|
+
? "ring-primary border-primary/50 ring-2 bg-white"
|
|
59
|
+
: data.isBlocked
|
|
60
|
+
? "border-red-200 bg-red-50/70"
|
|
61
|
+
: data.status === "Done"
|
|
62
|
+
? "border-emerald-200 bg-emerald-50/80"
|
|
63
|
+
: "border-slate-200 bg-white hover:border-slate-300 hover:shadow-md";
|
|
64
|
+
return (_jsxs("div", { className: cn("relative cursor-pointer rounded-lg border px-3 py-2 shadow-sm transition-all", nodeSurfaceClass), style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-node-${data.taskId}`, children: [_jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-left-${data.taskId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-top-${data.taskId}` }), _jsxs("div", { className: "flex items-start justify-between gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-between gap-1", children: [_jsx("span", { className: "truncate text-[10px] font-medium text-slate-400", children: data.taskKey || data.taskId.slice(0, 8).toUpperCase() }), PriorityIcon && (_jsx(PriorityIcon, { className: cn("h-3 w-3 shrink-0", priorityCfg.color) }))] }), data.onRemoveTask && (_jsx("button", { type: "button", className: "-mt-px -mr-0.5 shrink-0 rounded p-0.5 text-slate-400 transition-colors hover:bg-red-50 hover:text-red-600", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
|
|
52
65
|
event.stopPropagation();
|
|
53
66
|
data.onRemoveTask?.(data.taskId);
|
|
54
|
-
}, children: _jsx(Trash2, { className: "h-2.5 w-2.5", strokeWidth: 1.5 }) }))] }), _jsx("div", { className: "mt-0.5 truncate text-xs font-medium text-slate-800", children: data.title }), _jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, className: cn("h-5 max-w-full px-1.5 text-[10px] font-bold
|
|
67
|
+
}, children: _jsx(Trash2, { className: "h-2.5 w-2.5", strokeWidth: 1.5 }) }))] }), _jsx("div", { className: "mt-0.5 truncate text-xs font-medium text-slate-800", children: data.title }), _jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, className: cn("h-5 max-w-full px-1.5 text-[10px] font-bold tracking-wider uppercase", executionDisplay.className), children: [executionDisplay.showSpinner && (_jsx(Loader2, { className: "mr-1 h-2.5 w-2.5 animate-spin" })), executionDisplay.label] })) : (_jsxs("span", { className: cn("inline-flex items-center gap-0.5 rounded-full px-1.5 py-0 text-[10px] font-medium", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, { className: "h-2.5 w-2.5" }), getTaskStatusLabel(status)] })), data.assigneeDisplayName && (_jsxs("span", { className: "truncate text-[10px] text-slate-400", children: ["@", data.assigneeDisplayName] }))] }), data.onAddDependentTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, dependentButtonClass), "aria-label": "Add dependent task", "data-testid": `dependency-graph-add-dependent-task-${data.taskId}`, onClick: (event) => {
|
|
55
68
|
event.stopPropagation();
|
|
56
69
|
data.onSelect(data.taskId);
|
|
57
70
|
data.onAddDependentTaskFromNode?.(data.taskId);
|
|
58
71
|
}, onMouseDown: (event) => {
|
|
59
72
|
event.stopPropagation();
|
|
60
|
-
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side:
|
|
73
|
+
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: dependentTooltipSide, sideOffset: 8, children: "Add dependent task" })] })), data.onAddChildTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, subtaskButtonClass), "aria-label": "Add subtask", "data-testid": `dependency-graph-add-subtask-${data.taskId}`, onClick: (event) => {
|
|
61
74
|
event.stopPropagation();
|
|
62
75
|
data.onSelect(data.taskId);
|
|
63
76
|
data.onAddChildTaskFromNode?.(data.taskId);
|
|
64
77
|
}, onMouseDown: (event) => {
|
|
65
78
|
event.stopPropagation();
|
|
66
|
-
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side:
|
|
79
|
+
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: subtaskTooltipSide, sideOffset: 8, children: "Add subtask" })] })), _jsx(Handle, { type: "source", position: Position.Right, id: "right", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-right-${data.taskId}` }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-bottom-${data.taskId}` })] }));
|
|
67
80
|
}
|
|
68
81
|
const nodeTypes = {
|
|
69
82
|
taskNode: TaskGraphNode,
|
|
@@ -71,13 +84,30 @@ const nodeTypes = {
|
|
|
71
84
|
const HIERARCHY_CHILD_INDENT = 24;
|
|
72
85
|
const HIERARCHY_CHILD_GAP = 32;
|
|
73
86
|
const HIERARCHY_SUBTREE_GAP = 80;
|
|
87
|
+
function normalizeGraphOrientation(orientation) {
|
|
88
|
+
return orientation === "vertical" ? "vertical" : "horizontal";
|
|
89
|
+
}
|
|
90
|
+
function getDependencyHandles(orientation) {
|
|
91
|
+
return orientation === "horizontal"
|
|
92
|
+
? { source: "right", target: "left" }
|
|
93
|
+
: { source: "bottom", target: "top" };
|
|
94
|
+
}
|
|
95
|
+
function getHierarchyHandles(orientation, autoLayoutStrategy) {
|
|
96
|
+
if (autoLayoutStrategy !== "hierarchy") {
|
|
97
|
+
return getDependencyHandles(orientation);
|
|
98
|
+
}
|
|
99
|
+
return orientation === "horizontal"
|
|
100
|
+
? { source: "bottom", target: "left" }
|
|
101
|
+
: { source: "right", target: "left" };
|
|
102
|
+
}
|
|
74
103
|
/**
|
|
75
104
|
* Compound layout for the hierarchy strategy: each parent–children group is
|
|
76
105
|
* laid out vertically (parent on top, children stacked below with a slight
|
|
77
106
|
* indent), and then the resulting subtree "super-nodes" are arranged
|
|
78
|
-
*
|
|
107
|
+
* in the selected orientation via Dagre using the dependency edges between
|
|
108
|
+
* them.
|
|
79
109
|
*/
|
|
80
|
-
function layoutGraphHierarchy(nodes, edges) {
|
|
110
|
+
function layoutGraphHierarchy(nodes, edges, orientation) {
|
|
81
111
|
if (nodes.length === 0)
|
|
82
112
|
return { nodes, edges };
|
|
83
113
|
const childrenOf = new Map();
|
|
@@ -122,7 +152,11 @@ function layoutGraphHierarchy(nodes, edges) {
|
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
125
|
-
g.setGraph({
|
|
155
|
+
g.setGraph({
|
|
156
|
+
rankdir: orientation === "horizontal" ? "LR" : "TB",
|
|
157
|
+
nodesep: 40,
|
|
158
|
+
ranksep: HIERARCHY_SUBTREE_GAP,
|
|
159
|
+
});
|
|
126
160
|
for (const [rootId, data] of subtrees) {
|
|
127
161
|
g.setNode(rootId, { width: data.width, height: data.height });
|
|
128
162
|
}
|
|
@@ -164,7 +198,7 @@ function layoutGraphHierarchy(nodes, edges) {
|
|
|
164
198
|
function layoutGraph(nodes, edges, orientation, autoLayoutStrategy) {
|
|
165
199
|
if (autoLayoutStrategy === "hierarchy" &&
|
|
166
200
|
edges.some((e) => e.data?.relationship === "hierarchy")) {
|
|
167
|
-
return layoutGraphHierarchy(nodes, edges);
|
|
201
|
+
return layoutGraphHierarchy(nodes, edges, orientation);
|
|
168
202
|
}
|
|
169
203
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
170
204
|
g.setGraph({
|
|
@@ -199,6 +233,8 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
199
233
|
const childRelationshipKeys = new Set(visibleTasks
|
|
200
234
|
.filter((task) => !!task.parentTaskId)
|
|
201
235
|
.map((task) => `${task.parentTaskId}->${task.taskId}`));
|
|
236
|
+
const dependencyHandles = getDependencyHandles(orientation);
|
|
237
|
+
const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
|
|
202
238
|
const nodes = visibleTasks.map((task) => ({
|
|
203
239
|
id: task.taskId,
|
|
204
240
|
type: "taskNode",
|
|
@@ -215,26 +251,27 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
215
251
|
isSelected: task.taskId === selectedTaskId,
|
|
216
252
|
isBlocked: blockedTaskIds.has(task.taskId),
|
|
217
253
|
orientation,
|
|
254
|
+
dependencySourceHandle: dependencyHandles.source,
|
|
255
|
+
hierarchySourceHandle: hierarchyHandles.source,
|
|
218
256
|
onSelect,
|
|
219
257
|
onAddDependentTaskFromNode,
|
|
220
258
|
onAddChildTaskFromNode,
|
|
221
259
|
onRemoveTask,
|
|
222
260
|
},
|
|
223
261
|
}));
|
|
224
|
-
const depSourceHandle =
|
|
225
|
-
const depTargetHandle =
|
|
226
|
-
const hierSourceHandle =
|
|
227
|
-
const hierTargetHandle =
|
|
262
|
+
const depSourceHandle = dependencyHandles.source;
|
|
263
|
+
const depTargetHandle = dependencyHandles.target;
|
|
264
|
+
const hierSourceHandle = hierarchyHandles.source;
|
|
265
|
+
const hierTargetHandle = hierarchyHandles.target;
|
|
228
266
|
const dependencyEdges = dependencies
|
|
229
267
|
.filter((dep) => taskMap.has(dep.taskId) &&
|
|
230
268
|
taskMap.has(dep.dependsOnTaskId) &&
|
|
231
269
|
!childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
|
|
232
270
|
.map((dep) => {
|
|
233
271
|
const isBlockedBy = dep.dependencyType === "BlockedBy";
|
|
234
|
-
const strokeColor = isBlockedBy
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const isSelected = dep.taskId === selectedTaskId && dep.dependsOnTaskId === selectedDependencyId;
|
|
272
|
+
const strokeColor = isBlockedBy ? EDGE_BLOCKED_BY : EDGE_RELATED;
|
|
273
|
+
const isSelected = dep.taskId === selectedTaskId &&
|
|
274
|
+
dep.dependsOnTaskId === selectedDependencyId;
|
|
238
275
|
return {
|
|
239
276
|
id: dep.dependencyId,
|
|
240
277
|
source: dep.dependsOnTaskId,
|
|
@@ -264,8 +301,7 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
264
301
|
};
|
|
265
302
|
});
|
|
266
303
|
const hierarchyEdges = visibleTasks
|
|
267
|
-
.filter((task) => !!task.parentTaskId &&
|
|
268
|
-
taskMap.has(task.parentTaskId))
|
|
304
|
+
.filter((task) => !!task.parentTaskId && taskMap.has(task.parentTaskId))
|
|
269
305
|
.map((task) => ({
|
|
270
306
|
id: `parent:${task.parentTaskId}:${task.taskId}`,
|
|
271
307
|
source: task.parentTaskId,
|
|
@@ -320,12 +356,15 @@ function buildChildRelationshipKeys(tasks) {
|
|
|
320
356
|
.filter((task) => !!task.parentTaskId)
|
|
321
357
|
.map((task) => `${task.parentTaskId}->${task.taskId}`));
|
|
322
358
|
}
|
|
323
|
-
function isHierarchyConnectionValid(connection, tasks) {
|
|
359
|
+
function isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy) {
|
|
360
|
+
if (autoLayoutStrategy !== "hierarchy") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
324
363
|
const { source, sourceHandle, target, targetHandle } = connection;
|
|
325
364
|
if (!source || !target || source === target) {
|
|
326
365
|
return false;
|
|
327
366
|
}
|
|
328
|
-
if (sourceHandle !==
|
|
367
|
+
if (sourceHandle !== getHierarchyHandles(orientation, autoLayoutStrategy).source) {
|
|
329
368
|
return false;
|
|
330
369
|
}
|
|
331
370
|
if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
|
|
@@ -345,12 +384,12 @@ function isHierarchyConnectionValid(connection, tasks) {
|
|
|
345
384
|
}
|
|
346
385
|
return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
|
|
347
386
|
}
|
|
348
|
-
function isDependencyConnectionValid(connection, tasks, dependencies) {
|
|
387
|
+
function isDependencyConnectionValid(connection, tasks, dependencies, orientation) {
|
|
349
388
|
const { source, sourceHandle, target, targetHandle } = connection;
|
|
350
389
|
if (!source || !target || source === target) {
|
|
351
390
|
return false;
|
|
352
391
|
}
|
|
353
|
-
if (sourceHandle !==
|
|
392
|
+
if (sourceHandle !== getDependencyHandles(orientation).source) {
|
|
354
393
|
return false;
|
|
355
394
|
}
|
|
356
395
|
if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
|
|
@@ -365,15 +404,18 @@ function isDependencyConnectionValid(connection, tasks, dependencies) {
|
|
|
365
404
|
}
|
|
366
405
|
return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
|
|
367
406
|
}
|
|
368
|
-
function isGraphConnectionValid(connection, tasks, dependencies) {
|
|
369
|
-
|
|
370
|
-
|
|
407
|
+
function isGraphConnectionValid(connection, tasks, dependencies, orientation, autoLayoutStrategy) {
|
|
408
|
+
const hierarchySourceHandle = getHierarchyHandles(orientation, "hierarchy").source;
|
|
409
|
+
if (autoLayoutStrategy === "hierarchy" &&
|
|
410
|
+
connection.sourceHandle === hierarchySourceHandle) {
|
|
411
|
+
return isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy);
|
|
371
412
|
}
|
|
372
|
-
return isDependencyConnectionValid(connection, tasks, dependencies);
|
|
413
|
+
return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
|
|
373
414
|
}
|
|
374
|
-
function createLayoutSignature(layoutKey, nodes, viewport) {
|
|
415
|
+
function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
|
|
375
416
|
return JSON.stringify({
|
|
376
417
|
layoutKey,
|
|
418
|
+
orientation,
|
|
377
419
|
nodes: nodes
|
|
378
420
|
.map((node) => ({
|
|
379
421
|
taskId: node.id,
|
|
@@ -391,11 +433,17 @@ function createLayoutSignature(layoutKey, nodes, viewport) {
|
|
|
391
433
|
});
|
|
392
434
|
}
|
|
393
435
|
export function DependencyGraphView(props) {
|
|
394
|
-
const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, selectedDependencyId = null, onSelectTask, onSelectDependency, onClearDependencySelection, permission, canPersistLayout: canPersistLayoutOverride, onPersistLayout, onLayoutSaved, highlightBlockedTasks = true, showExecutionStateBadges = true, emptyStateTitle = "No tasks to visualize", emptyStateDescription = "Create tasks in this project to see them in the dependency graph.", orientation = "vertical", autoLayoutStrategy = "allEdges", layoutSaveDebounceMs = SAVE_DEBOUNCE_MS, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, allowDependencyConnect = false, onCreateDependency, onCreateChildRelationship, miniMapWidth, miniMapHeight, } = props;
|
|
436
|
+
const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, selectedDependencyId = null, onSelectTask, onSelectDependency, onClearDependencySelection, permission, canPersistLayout: canPersistLayoutOverride, onPersistLayout, onLayoutSaved, highlightBlockedTasks = true, showExecutionStateBadges = true, emptyStateTitle = "No tasks to visualize", emptyStateDescription = "Create tasks in this project to see them in the dependency graph.", orientation = "vertical", autoLayoutStrategy = "allEdges", showOrientationToggle = false, layoutSaveDebounceMs = SAVE_DEBOUNCE_MS, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, allowDependencyConnect = false, onCreateDependency, onCreateChildRelationship, miniMapWidth, miniMapHeight, showMiniMap = true, } = props;
|
|
395
437
|
const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
|
|
396
438
|
? canPersistLayoutOverride
|
|
397
439
|
: permission === "Owner" || permission === "Editor";
|
|
398
440
|
const effectiveLayoutKey = layoutKey ?? projectId ?? "graph";
|
|
441
|
+
const fallbackOrientation = normalizeGraphOrientation(orientation);
|
|
442
|
+
const savedLayoutOrientation = savedLayout
|
|
443
|
+
? normalizeGraphOrientation(savedLayout.orientation ?? fallbackOrientation)
|
|
444
|
+
: null;
|
|
445
|
+
const [transientOrientation, setTransientOrientation] = useState(null);
|
|
446
|
+
const effectiveOrientation = transientOrientation ?? savedLayoutOrientation ?? fallbackOrientation;
|
|
399
447
|
const taskMap = useMemo(() => {
|
|
400
448
|
const m = new Map();
|
|
401
449
|
for (const t of tasks)
|
|
@@ -424,13 +472,13 @@ export function DependencyGraphView(props) {
|
|
|
424
472
|
return blocked;
|
|
425
473
|
}, [dependencies, highlightBlockedTasks, taskMap]);
|
|
426
474
|
const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
|
|
427
|
-
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds,
|
|
475
|
+
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
|
|
428
476
|
tasks,
|
|
429
477
|
dependencies,
|
|
430
478
|
selectedTaskId,
|
|
431
479
|
selectedDependencyId,
|
|
432
480
|
blockedTaskIds,
|
|
433
|
-
|
|
481
|
+
effectiveOrientation,
|
|
434
482
|
showExecutionStateBadges,
|
|
435
483
|
autoLayoutStrategy,
|
|
436
484
|
onSelect,
|
|
@@ -458,22 +506,34 @@ export function DependencyGraphView(props) {
|
|
|
458
506
|
const pendingViewportRef = useRef(null);
|
|
459
507
|
const fitViewOnNextSyncRef = useRef(false);
|
|
460
508
|
const skipNextMoveEndSaveRef = useRef(false);
|
|
461
|
-
const
|
|
509
|
+
const pendingSaveOrientationRef = useRef(null);
|
|
462
510
|
const lastSavedSignatureRef = useRef(null);
|
|
463
511
|
const lastAppliedLayoutKeyRef = useRef(null);
|
|
464
512
|
const lastAppliedLayoutSignatureRef = useRef(null);
|
|
465
513
|
const savedLayoutSignature = useMemo(() => savedLayout
|
|
466
514
|
? JSON.stringify({
|
|
515
|
+
orientation: savedLayoutOrientation,
|
|
467
516
|
updatedAt: savedLayout.updatedAt ?? null,
|
|
468
517
|
nodes: savedLayout.nodes ?? [],
|
|
469
518
|
viewport: savedLayout.viewport ?? null,
|
|
470
519
|
})
|
|
471
|
-
: "null", [savedLayout]);
|
|
472
|
-
|
|
520
|
+
: "null", [savedLayout, savedLayoutOrientation]);
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
setTransientOrientation(null);
|
|
523
|
+
pendingSaveOrientationRef.current = null;
|
|
524
|
+
}, [effectiveLayoutKey]);
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
if (transientOrientation &&
|
|
527
|
+
savedLayoutOrientation &&
|
|
528
|
+
transientOrientation === savedLayoutOrientation) {
|
|
529
|
+
setTransientOrientation(null);
|
|
530
|
+
}
|
|
531
|
+
}, [savedLayoutOrientation, transientOrientation]);
|
|
532
|
+
const persistLayout = useCallback(async (nextNodes, viewport, nextOrientation) => {
|
|
473
533
|
if (!canPersistLayout) {
|
|
474
534
|
return;
|
|
475
535
|
}
|
|
476
|
-
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
|
|
536
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
|
|
477
537
|
if (signature === lastSavedSignatureRef.current) {
|
|
478
538
|
return;
|
|
479
539
|
}
|
|
@@ -484,6 +544,7 @@ export function DependencyGraphView(props) {
|
|
|
484
544
|
x: node.position.x,
|
|
485
545
|
y: node.position.y,
|
|
486
546
|
})),
|
|
547
|
+
orientation: nextOrientation,
|
|
487
548
|
viewport: viewport == null
|
|
488
549
|
? null
|
|
489
550
|
: {
|
|
@@ -500,6 +561,7 @@ export function DependencyGraphView(props) {
|
|
|
500
561
|
projectId,
|
|
501
562
|
nodes: layoutPayload.nodes,
|
|
502
563
|
viewport: layoutPayload.viewport,
|
|
564
|
+
orientation: layoutPayload.orientation,
|
|
503
565
|
});
|
|
504
566
|
if (result.type !== "success") {
|
|
505
567
|
toast.error(result.summary || "Failed to save graph layout");
|
|
@@ -511,6 +573,7 @@ export function DependencyGraphView(props) {
|
|
|
511
573
|
lastSavedSignatureRef.current = signature;
|
|
512
574
|
if (nextLayout) {
|
|
513
575
|
const incomingSignature = JSON.stringify({
|
|
576
|
+
orientation: normalizeGraphOrientation(nextLayout.orientation ?? nextOrientation),
|
|
514
577
|
updatedAt: nextLayout.updatedAt ?? null,
|
|
515
578
|
nodes: nextLayout.nodes ?? [],
|
|
516
579
|
viewport: nextLayout.viewport ?? null,
|
|
@@ -527,15 +590,16 @@ export function DependencyGraphView(props) {
|
|
|
527
590
|
}, [
|
|
528
591
|
canPersistLayout,
|
|
529
592
|
effectiveLayoutKey,
|
|
593
|
+
effectiveOrientation,
|
|
530
594
|
onLayoutSaved,
|
|
531
595
|
onPersistLayout,
|
|
532
596
|
projectId,
|
|
533
597
|
]);
|
|
534
|
-
const queueLayoutSave = useCallback((nextNodes, viewport) => {
|
|
598
|
+
const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
|
|
535
599
|
if (!canPersistLayout) {
|
|
536
600
|
return;
|
|
537
601
|
}
|
|
538
|
-
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
|
|
602
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
|
|
539
603
|
if (signature === lastSavedSignatureRef.current) {
|
|
540
604
|
return;
|
|
541
605
|
}
|
|
@@ -544,14 +608,20 @@ export function DependencyGraphView(props) {
|
|
|
544
608
|
saveTimeoutRef.current = null;
|
|
545
609
|
}
|
|
546
610
|
if (layoutSaveDebounceMs === 0) {
|
|
547
|
-
void persistLayout(nextNodes, viewport);
|
|
611
|
+
void persistLayout(nextNodes, viewport, nextOrientation);
|
|
548
612
|
return;
|
|
549
613
|
}
|
|
550
614
|
saveTimeoutRef.current = window.setTimeout(() => {
|
|
551
615
|
saveTimeoutRef.current = null;
|
|
552
|
-
void persistLayout(nextNodes, viewport);
|
|
616
|
+
void persistLayout(nextNodes, viewport, nextOrientation);
|
|
553
617
|
}, layoutSaveDebounceMs);
|
|
554
|
-
}, [
|
|
618
|
+
}, [
|
|
619
|
+
canPersistLayout,
|
|
620
|
+
effectiveLayoutKey,
|
|
621
|
+
effectiveOrientation,
|
|
622
|
+
layoutSaveDebounceMs,
|
|
623
|
+
persistLayout,
|
|
624
|
+
]);
|
|
555
625
|
const restoreViewport = useCallback(() => {
|
|
556
626
|
const instance = reactFlowRef.current;
|
|
557
627
|
if (!instance || nodesRef.current.length === 0) {
|
|
@@ -563,7 +633,7 @@ export function DependencyGraphView(props) {
|
|
|
563
633
|
skipNextMoveEndSaveRef.current = true;
|
|
564
634
|
window.requestAnimationFrame(() => {
|
|
565
635
|
reactFlowRef.current?.setViewport(viewport, { duration: 0 });
|
|
566
|
-
lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport);
|
|
636
|
+
lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
|
|
567
637
|
});
|
|
568
638
|
return;
|
|
569
639
|
}
|
|
@@ -574,14 +644,15 @@ export function DependencyGraphView(props) {
|
|
|
574
644
|
skipNextMoveEndSaveRef.current = true;
|
|
575
645
|
window.requestAnimationFrame(() => {
|
|
576
646
|
reactFlowRef.current?.fitView({ padding: 0.2 });
|
|
577
|
-
if (!
|
|
647
|
+
if (!pendingSaveOrientationRef.current) {
|
|
578
648
|
return;
|
|
579
649
|
}
|
|
580
|
-
|
|
650
|
+
const nextOrientation = pendingSaveOrientationRef.current;
|
|
651
|
+
pendingSaveOrientationRef.current = null;
|
|
581
652
|
const viewport = reactFlowRef.current?.getViewport() ?? null;
|
|
582
|
-
queueLayoutSave(nodesRef.current, viewport);
|
|
653
|
+
queueLayoutSave(nodesRef.current, viewport, nextOrientation);
|
|
583
654
|
});
|
|
584
|
-
}, [effectiveLayoutKey, queueLayoutSave]);
|
|
655
|
+
}, [effectiveLayoutKey, effectiveOrientation, queueLayoutSave]);
|
|
585
656
|
useEffect(() => {
|
|
586
657
|
const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
|
|
587
658
|
lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
|
|
@@ -598,7 +669,7 @@ export function DependencyGraphView(props) {
|
|
|
598
669
|
fitViewOnNextSyncRef.current = !savedLayout?.viewport;
|
|
599
670
|
if (!savedLayout?.viewport) {
|
|
600
671
|
lastSavedSignatureRef.current = savedLayout?.nodes?.length
|
|
601
|
-
? createLayoutSignature(effectiveLayoutKey, nextNodes, null)
|
|
672
|
+
? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
|
|
602
673
|
: null;
|
|
603
674
|
}
|
|
604
675
|
restoreViewport();
|
|
@@ -607,6 +678,7 @@ export function DependencyGraphView(props) {
|
|
|
607
678
|
initialEdges,
|
|
608
679
|
initialNodes,
|
|
609
680
|
effectiveLayoutKey,
|
|
681
|
+
effectiveOrientation,
|
|
610
682
|
restoreViewport,
|
|
611
683
|
savedLayout,
|
|
612
684
|
savedLayoutSignature,
|
|
@@ -623,18 +695,18 @@ export function DependencyGraphView(props) {
|
|
|
623
695
|
}, []);
|
|
624
696
|
const hasVisibleNodes = initialNodes.length > 0;
|
|
625
697
|
const handleAutoLayout = useCallback(() => {
|
|
626
|
-
const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds,
|
|
698
|
+
const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
|
|
627
699
|
setNodes(autoLayout.nodes);
|
|
628
700
|
setEdges(autoLayout.edges);
|
|
629
701
|
nodesRef.current = autoLayout.nodes;
|
|
630
702
|
fitViewOnNextSyncRef.current = true;
|
|
631
|
-
|
|
703
|
+
pendingSaveOrientationRef.current = effectiveOrientation;
|
|
632
704
|
restoreViewport();
|
|
633
705
|
}, [
|
|
634
706
|
blockedTaskIds,
|
|
635
707
|
dependencies,
|
|
708
|
+
effectiveOrientation,
|
|
636
709
|
onSelect,
|
|
637
|
-
orientation,
|
|
638
710
|
showExecutionStateBadges,
|
|
639
711
|
autoLayoutStrategy,
|
|
640
712
|
onRemoveTask,
|
|
@@ -648,8 +720,14 @@ export function DependencyGraphView(props) {
|
|
|
648
720
|
tasks,
|
|
649
721
|
]);
|
|
650
722
|
const isValidConnection = useCallback((connection) => allowDependencyConnect
|
|
651
|
-
? isGraphConnectionValid(connection, tasks, dependencies)
|
|
652
|
-
: false, [
|
|
723
|
+
? isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)
|
|
724
|
+
: false, [
|
|
725
|
+
allowDependencyConnect,
|
|
726
|
+
autoLayoutStrategy,
|
|
727
|
+
dependencies,
|
|
728
|
+
effectiveOrientation,
|
|
729
|
+
tasks,
|
|
730
|
+
]);
|
|
653
731
|
const handleConnect = useCallback((connection) => {
|
|
654
732
|
if (!allowDependencyConnect) {
|
|
655
733
|
return;
|
|
@@ -658,24 +736,60 @@ export function DependencyGraphView(props) {
|
|
|
658
736
|
if (!source || !target) {
|
|
659
737
|
return;
|
|
660
738
|
}
|
|
661
|
-
if (!isGraphConnectionValid(connection, tasks, dependencies)) {
|
|
739
|
+
if (!isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)) {
|
|
662
740
|
return;
|
|
663
741
|
}
|
|
664
742
|
onSelectTask(target);
|
|
665
|
-
if (
|
|
743
|
+
if (autoLayoutStrategy === "hierarchy" &&
|
|
744
|
+
sourceHandle === getHierarchyHandles(effectiveOrientation, "hierarchy").source) {
|
|
666
745
|
onCreateChildRelationship?.(source, target);
|
|
667
746
|
return;
|
|
668
747
|
}
|
|
669
748
|
onCreateDependency?.(source, target);
|
|
670
749
|
}, [
|
|
671
750
|
allowDependencyConnect,
|
|
751
|
+
autoLayoutStrategy,
|
|
672
752
|
dependencies,
|
|
753
|
+
effectiveOrientation,
|
|
673
754
|
onCreateChildRelationship,
|
|
674
755
|
onCreateDependency,
|
|
675
756
|
onSelectTask,
|
|
676
757
|
tasks,
|
|
677
758
|
]);
|
|
678
|
-
|
|
759
|
+
const handleOrientationChange = useCallback((nextOrientation) => {
|
|
760
|
+
if (nextOrientation === effectiveOrientation) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
setTransientOrientation(nextOrientation);
|
|
764
|
+
const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, nextOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
|
|
765
|
+
setNodes(autoLayout.nodes);
|
|
766
|
+
setEdges(autoLayout.edges);
|
|
767
|
+
nodesRef.current = autoLayout.nodes;
|
|
768
|
+
fitViewOnNextSyncRef.current = true;
|
|
769
|
+
pendingSaveOrientationRef.current = nextOrientation;
|
|
770
|
+
restoreViewport();
|
|
771
|
+
}, [
|
|
772
|
+
autoLayoutStrategy,
|
|
773
|
+
blockedTaskIds,
|
|
774
|
+
dependencies,
|
|
775
|
+
effectiveOrientation,
|
|
776
|
+
onAddChildTaskFromNode,
|
|
777
|
+
onAddDependentTaskFromNode,
|
|
778
|
+
onRemoveTask,
|
|
779
|
+
onSelect,
|
|
780
|
+
restoreViewport,
|
|
781
|
+
selectedDependencyId,
|
|
782
|
+
selectedTaskId,
|
|
783
|
+
setEdges,
|
|
784
|
+
setNodes,
|
|
785
|
+
showExecutionStateBadges,
|
|
786
|
+
tasks,
|
|
787
|
+
]);
|
|
788
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 px-3 pt-2", children: [hasVisibleNodes && highlightBlockedTasks ? (_jsxs("div", { className: "flex items-center gap-2 text-[11px] text-slate-400", children: [_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-orange-400" }), "Blocked by"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-slate-400" }), "Related"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-blue-500" }), "Child task"] })] })) : (_jsx("div", {})), hasVisibleNodes && (_jsxs("div", { className: "flex items-center gap-2", children: [showOrientationToggle && (_jsxs("div", { className: "inline-flex rounded-md border border-slate-200 bg-white p-0.5", "data-testid": "dependency-graph-orientation-toggle", children: [_jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "horizontal"
|
|
789
|
+
? "bg-slate-900 text-white"
|
|
790
|
+
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-horizontal", onClick: () => handleOrientationChange("horizontal"), children: "Horizontal" }), _jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "vertical"
|
|
791
|
+
? "bg-slate-900 text-white"
|
|
792
|
+
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-vertical", onClick: () => handleOrientationChange("vertical"), children: "Vertical" })] })), canPersistLayout && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 px-3", onClick: handleAutoLayout, children: [_jsx(RotateCcw, { className: "mr-1.5 h-4 w-4" }), "Auto layout"] }))] }))] }), _jsx("div", { className: "min-h-0 flex-1", children: !hasVisibleNodes ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 text-center", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-slate-100", children: _jsx(GitBranch, { className: "h-6 w-6 text-slate-400" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-slate-600", children: emptyStateTitle }), _jsx("p", { className: "mt-1 text-xs text-slate-400", children: emptyStateDescription })] })] }) })) : (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: handleConnect, onPaneClick: () => {
|
|
679
793
|
onClearDependencySelection?.();
|
|
680
794
|
}, onEdgeClick: (_event, edge) => {
|
|
681
795
|
if (edge.data?.relationship === "dependency" &&
|
|
@@ -702,13 +816,16 @@ export function DependencyGraphView(props) {
|
|
|
702
816
|
return;
|
|
703
817
|
}
|
|
704
818
|
queueLayoutSave(nodesRef.current, viewport);
|
|
705
|
-
}, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), _jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
|
|
819
|
+
}, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), showMiniMap ? (_jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
|
|
706
820
|
? { width: miniMapWidth, height: miniMapHeight }
|
|
707
821
|
: {}), nodeColor: (node) => {
|
|
708
|
-
const
|
|
822
|
+
const d = node.data;
|
|
823
|
+
if (d?.isBlocked)
|
|
824
|
+
return "#fecaca";
|
|
825
|
+
const status = d?.status;
|
|
709
826
|
switch (status) {
|
|
710
827
|
case "Done":
|
|
711
|
-
return "#
|
|
828
|
+
return "#bbf7d0";
|
|
712
829
|
case "InProgress":
|
|
713
830
|
return "#93c5fd";
|
|
714
831
|
case "Review":
|
|
@@ -718,6 +835,6 @@ export function DependencyGraphView(props) {
|
|
|
718
835
|
default:
|
|
719
836
|
return "#cbd5e1";
|
|
720
837
|
}
|
|
721
|
-
}, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })] })) })] }));
|
|
838
|
+
}, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })) : null] })) })] }));
|
|
722
839
|
}
|
|
723
840
|
//# sourceMappingURL=DependencyGraphView.js.map
|