@parhelia/core 0.3.12815 → 0.3.12816
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/agents-view/AgentsView.js +10 -11
- package/dist/agents-view/AgentsView.js.map +1 -1
- package/dist/components/ui/LanguageSelector.js +12 -1
- package/dist/components/ui/LanguageSelector.js.map +1 -1
- package/dist/config/config.js +11 -1
- package/dist/config/config.js.map +1 -1
- package/dist/editor/ContentTree.js +8 -3
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/FieldListFieldWithFallbacks.js +1 -1
- package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
- package/dist/editor/LinkEditorDialog.js +1 -5
- package/dist/editor/LinkEditorDialog.js.map +1 -1
- package/dist/editor/ai-image-editor/AiImageEditorDialog.js +57 -39
- package/dist/editor/ai-image-editor/AiImageEditorDialog.js.map +1 -1
- package/dist/editor/client/EditorShell.js +1 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +6 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/itemsRepository.d.ts +2 -0
- package/dist/editor/client/itemsRepository.js +64 -0
- package/dist/editor/client/itemsRepository.js.map +1 -1
- package/dist/editor/client/operations.js +48 -5
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/commands/deleteVersionCommand.js +1 -0
- package/dist/editor/commands/deleteVersionCommand.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +48 -3
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/media-selector/TreeSelector.js +39 -20
- package/dist/editor/media-selector/TreeSelector.js.map +1 -1
- package/dist/editor/menubar/PageSelector.js +2 -8
- package/dist/editor/menubar/PageSelector.js.map +1 -1
- package/dist/editor/menubar/pageSelectorHistoryFilter.d.ts +9 -0
- package/dist/editor/menubar/pageSelectorHistoryFilter.js +40 -6
- package/dist/editor/menubar/pageSelectorHistoryFilter.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +17 -6
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/page-viewer/internalLinkNavigation.d.ts +2 -0
- package/dist/editor/page-viewer/internalLinkNavigation.js +10 -0
- package/dist/editor/page-viewer/internalLinkNavigation.js.map +1 -1
- package/dist/editor/reviews/ReviewersPanel.js +1 -1
- package/dist/editor/reviews/ReviewersPanel.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +20 -0
- package/dist/editor/services/aiService.js +12 -0
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/editor/settings/UsageLeaderboard.d.ts +2 -0
- package/dist/editor/settings/UsageLeaderboard.js +108 -0
- package/dist/editor/settings/UsageLeaderboard.js.map +1 -0
- package/dist/editor/setup-wizard/steps/CompleteStep.js +2 -2
- package/dist/editor/setup-wizard/steps/CompleteStep.js.map +1 -1
- package/dist/editor/setup-wizard/steps/ModelSelectionStep.d.ts +1 -1
- package/dist/editor/setup-wizard/steps/ModelSelectionStep.js +6 -6
- package/dist/editor/setup-wizard/steps/ModelSelectionStep.js.map +1 -1
- package/dist/editor/setup-wizard/steps/ProviderStep.js +7 -7
- package/dist/editor/setup-wizard/steps/ProviderStep.js.map +1 -1
- package/dist/editor/sidebar/ComponentPalette.js +15 -11
- package/dist/editor/sidebar/ComponentPalette.js.map +1 -1
- package/dist/editor/template-wizard/TemplateBuilderDialog.js +6 -2
- package/dist/editor/template-wizard/TemplateBuilderDialog.js.map +1 -1
- package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +6 -6
- package/dist/editor/template-wizard/TemplateStructureInlineEditor.js.map +1 -1
- package/dist/editor/ui/ItemSearch.d.ts +1 -1
- package/dist/editor/ui/ItemSearch.js +4 -0
- package/dist/editor/ui/ItemSearch.js.map +1 -1
- package/dist/editor/ui/PerfectTree.d.ts +3 -1
- package/dist/editor/ui/PerfectTree.js +8 -7
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/licensing/LicenseContext.js +14 -7
- package/dist/licensing/LicenseContext.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/task-board/types.d.ts +1 -0
- package/dist/task-board/views/DependencyGraphView.d.ts +42 -4
- package/dist/task-board/views/DependencyGraphView.js +467 -95
- package/dist/task-board/views/DependencyGraphView.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@ import { getTaskExecutionDisplayFromTask } from "../taskExecutionStatus";
|
|
|
10
10
|
import { cn } from "../../lib/utils";
|
|
11
11
|
import { saveGraphLayout } from "../services/taskService";
|
|
12
12
|
import { Button } from "../../components/ui/button";
|
|
13
|
-
import { Badge } from "../../components/ui/badge";
|
|
13
|
+
import { Badge, CountBadge } from "../../components/ui/badge";
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover";
|
|
14
15
|
import { MultiSwitchButton } from "../../components/ui/multi-switch-button";
|
|
15
16
|
import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
|
|
16
17
|
import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, GitBranch, RotateCcw, Loader2, Plus, Maximize2, Minimize2, SquareSquare, AlertCircle, MoveHorizontal, MoveVertical, } from "lucide-react";
|
|
@@ -18,7 +19,13 @@ import { DeleteIcon } from "../../components/ui/delete-icon";
|
|
|
18
19
|
import { TaskAssigneeBadge } from "../components/TaskAssigneeBadge";
|
|
19
20
|
const NODE_WIDTH = 220;
|
|
20
21
|
const NODE_HEIGHT = 96;
|
|
22
|
+
const GROUP_NODE_HEIGHT = 120;
|
|
21
23
|
const SAVE_DEBOUNCE_MS = 500;
|
|
24
|
+
const TASK_GROUP_NODE_ID_PREFIX = "taskgroup:";
|
|
25
|
+
const TEMPLATE_GROUP_MIN_SIZE = 4;
|
|
26
|
+
function isTaskGroupNodeId(nodeId) {
|
|
27
|
+
return nodeId.startsWith(TASK_GROUP_NODE_ID_PREFIX);
|
|
28
|
+
}
|
|
22
29
|
const EDGE_BLOCKED_BY = "var(--color-component-shared)";
|
|
23
30
|
const EDGE_CHILD_TASK = "var(--color-component-suggestions)";
|
|
24
31
|
const EDGE_RELATED = "var(--color-neutral-grey-50)";
|
|
@@ -62,6 +69,140 @@ const PRIORITY_HOVER_STYLES = {
|
|
|
62
69
|
};
|
|
63
70
|
const DEFAULT_HOVER_STYLE = "hover:border-neutral-grey-10/50";
|
|
64
71
|
const HANDLE_CLASS = "bg-neutral-grey-10! border-border-default! w-2! h-2!";
|
|
72
|
+
function getTaskGroupLabel(members) {
|
|
73
|
+
const titles = members.map((member) => member.title.trim()).filter(Boolean);
|
|
74
|
+
if (titles.length === 0)
|
|
75
|
+
return `${members.length} tasks`;
|
|
76
|
+
if (titles.every((title) => title === titles[0]))
|
|
77
|
+
return titles[0];
|
|
78
|
+
let prefix = titles[0];
|
|
79
|
+
for (const title of titles.slice(1)) {
|
|
80
|
+
let prefixLength = 0;
|
|
81
|
+
const maxLength = Math.min(prefix.length, title.length);
|
|
82
|
+
while (prefixLength < maxLength &&
|
|
83
|
+
prefix[prefixLength] === title[prefixLength]) {
|
|
84
|
+
prefixLength += 1;
|
|
85
|
+
}
|
|
86
|
+
prefix = prefix.slice(0, prefixLength);
|
|
87
|
+
if (!prefix)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const trimmedPrefix = prefix.replace(/[\s\p{P}\p{S}]+$/gu, "").trim();
|
|
91
|
+
return trimmedPrefix || `${members.length} tasks`;
|
|
92
|
+
}
|
|
93
|
+
function collectBlockedTaskIds(tasks, dependencies) {
|
|
94
|
+
const taskMap = new Map(tasks.map((task) => [task.taskId, task]));
|
|
95
|
+
const blocked = new Set();
|
|
96
|
+
for (const task of tasks) {
|
|
97
|
+
if ((task.executionBlockedByProjectIds?.length ?? 0) > 0) {
|
|
98
|
+
blocked.add(task.taskId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const dependency of dependencies) {
|
|
102
|
+
if (dependency.dependencyType !== "BlockedBy")
|
|
103
|
+
continue;
|
|
104
|
+
const blocker = taskMap.get(dependency.dependsOnTaskId);
|
|
105
|
+
if (!blocker ||
|
|
106
|
+
normalizeTaskStatus(blocker.status, blocker.executionState) !== "Done") {
|
|
107
|
+
blocked.add(dependency.taskId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return blocked;
|
|
111
|
+
}
|
|
112
|
+
function collectTaskGroups(tasks, selectedTaskId, blockedTaskIds) {
|
|
113
|
+
const groupedTasks = new Map();
|
|
114
|
+
const taskIdsWithChildren = new Set(tasks
|
|
115
|
+
.map((task) => task.parentTaskId?.trim())
|
|
116
|
+
.filter((taskId) => !!taskId));
|
|
117
|
+
for (const task of tasks) {
|
|
118
|
+
const parentTaskId = task.parentTaskId?.trim();
|
|
119
|
+
const batchId = task.batchId?.trim();
|
|
120
|
+
if (!parentTaskId || !batchId)
|
|
121
|
+
continue;
|
|
122
|
+
const groupId = `${TASK_GROUP_NODE_ID_PREFIX}${parentTaskId}:${batchId}`;
|
|
123
|
+
const members = groupedTasks.get(groupId) ?? [];
|
|
124
|
+
members.push(task);
|
|
125
|
+
groupedTasks.set(groupId, members);
|
|
126
|
+
}
|
|
127
|
+
const groups = new Map();
|
|
128
|
+
const collapsedTargetByTaskId = new Map();
|
|
129
|
+
for (const [groupId, tasksInGroup] of groupedTasks) {
|
|
130
|
+
if (tasksInGroup.length < TEMPLATE_GROUP_MIN_SIZE)
|
|
131
|
+
continue;
|
|
132
|
+
const members = tasksInGroup.map((task) => {
|
|
133
|
+
const status = normalizeTaskStatus(task.status, task.executionState);
|
|
134
|
+
const isBlocked = blockedTaskIds.has(task.taskId) ||
|
|
135
|
+
task.executionState === "WaitingForDependency";
|
|
136
|
+
const needsAttention = Boolean(getTaskExecutionDisplayFromTask({
|
|
137
|
+
taskStatus: status,
|
|
138
|
+
executionState: task.executionState,
|
|
139
|
+
isBlocked,
|
|
140
|
+
})?.needsAttention);
|
|
141
|
+
return {
|
|
142
|
+
taskId: task.taskId,
|
|
143
|
+
taskKey: task.taskKey ?? null,
|
|
144
|
+
title: task.title,
|
|
145
|
+
status,
|
|
146
|
+
executionState: task.executionState,
|
|
147
|
+
isBlocked,
|
|
148
|
+
isOutlier: task.taskId === selectedTaskId ||
|
|
149
|
+
(!isBlocked && needsAttention) ||
|
|
150
|
+
taskIdsWithChildren.has(task.taskId),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
const collapsedMembers = members.filter((member) => !member.isOutlier);
|
|
154
|
+
if (collapsedMembers.length < 2)
|
|
155
|
+
continue;
|
|
156
|
+
const statusCounts = {
|
|
157
|
+
done: 0,
|
|
158
|
+
inProgress: 0,
|
|
159
|
+
needsAttention: 0,
|
|
160
|
+
todo: 0,
|
|
161
|
+
};
|
|
162
|
+
for (const member of members) {
|
|
163
|
+
if (member.status === "Done") {
|
|
164
|
+
statusCounts.done += 1;
|
|
165
|
+
}
|
|
166
|
+
else if (member.isBlocked) {
|
|
167
|
+
statusCounts.todo += 1;
|
|
168
|
+
}
|
|
169
|
+
else if (getTaskExecutionDisplayFromTask({
|
|
170
|
+
taskStatus: member.status,
|
|
171
|
+
executionState: member.executionState,
|
|
172
|
+
isBlocked: member.isBlocked,
|
|
173
|
+
})?.needsAttention) {
|
|
174
|
+
statusCounts.needsAttention += 1;
|
|
175
|
+
}
|
|
176
|
+
else if (member.status === "InProgress" || member.status === "Review") {
|
|
177
|
+
statusCounts.inProgress += 1;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
statusCounts.todo += 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const firstTask = tasksInGroup[0];
|
|
184
|
+
const status = members.every((member) => member.status === "Done")
|
|
185
|
+
? "Done"
|
|
186
|
+
: members.some((member) => member.status === "InProgress" || member.status === "Review")
|
|
187
|
+
? "InProgress"
|
|
188
|
+
: "Todo";
|
|
189
|
+
groups.set(groupId, {
|
|
190
|
+
groupId,
|
|
191
|
+
parentTaskId: firstTask.parentTaskId.trim(),
|
|
192
|
+
batchId: firstTask.batchId.trim(),
|
|
193
|
+
label: getTaskGroupLabel(members),
|
|
194
|
+
memberCount: members.length,
|
|
195
|
+
statusCounts,
|
|
196
|
+
members,
|
|
197
|
+
status,
|
|
198
|
+
isBlocked: members.some((member) => member.isBlocked),
|
|
199
|
+
});
|
|
200
|
+
for (const member of collapsedMembers) {
|
|
201
|
+
collapsedTargetByTaskId.set(member.taskId, groupId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { groups, collapsedTargetByTaskId };
|
|
205
|
+
}
|
|
65
206
|
function TaskGraphNode({ data }) {
|
|
66
207
|
const status = data.status;
|
|
67
208
|
const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.Todo;
|
|
@@ -114,8 +255,61 @@ function TaskGraphNode({ data }) {
|
|
|
114
255
|
event.stopPropagation();
|
|
115
256
|
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _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}` })] }));
|
|
116
257
|
}
|
|
258
|
+
function TaskGroupGraphNode({ data }) {
|
|
259
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
260
|
+
const breakdown = [
|
|
261
|
+
{
|
|
262
|
+
key: "done",
|
|
263
|
+
label: "Done",
|
|
264
|
+
count: data.statusCounts.done,
|
|
265
|
+
icon: STATUS_STYLES.Done.icon,
|
|
266
|
+
className: STATUS_STYLES.Done.text,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: "inProgress",
|
|
270
|
+
label: "In progress",
|
|
271
|
+
count: data.statusCounts.inProgress,
|
|
272
|
+
icon: STATUS_STYLES.InProgress.icon,
|
|
273
|
+
className: STATUS_STYLES.InProgress.text,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
key: "needsAttention",
|
|
277
|
+
label: "Needs attention",
|
|
278
|
+
count: data.statusCounts.needsAttention,
|
|
279
|
+
icon: AlertCircle,
|
|
280
|
+
className: "text-feedback-red",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: "todo",
|
|
284
|
+
label: "To do",
|
|
285
|
+
count: data.statusCounts.todo,
|
|
286
|
+
icon: STATUS_STYLES.Todo.icon,
|
|
287
|
+
className: STATUS_STYLES.Todo.text,
|
|
288
|
+
},
|
|
289
|
+
].filter((item) => item.count > 0);
|
|
290
|
+
return (_jsxs(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [_jsxs("div", { className: "relative overflow-visible", style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-group-node-${data.groupId}`, children: [_jsx("div", { className: "border-border-default bg-neutral-grey-5 absolute inset-0 translate-x-2 translate-y-2 rounded-lg border shadow-sm" }), _jsx("div", { className: "border-border-default absolute inset-0 translate-x-1 translate-y-1 rounded-lg border bg-white shadow-sm" }), _jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-target-left-${data.groupId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-target-top-${data.groupId}` }), _jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "border-border-default relative z-10 flex min-h-30 w-full cursor-pointer flex-col gap-2.5 rounded-lg border bg-white p-3 text-left shadow-sm transition-all hover:shadow-md", "aria-label": `Show ${data.memberCount} tasks in ${data.label}`, children: [_jsxs("div", { className: "flex w-full min-w-0 items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-muted-foreground mb-1 text-[10px] font-medium tracking-widest uppercase", children: "Batch" }), _jsx("div", { className: "line-clamp-2 text-sm leading-snug font-medium", children: data.label })] }), _jsx(CountBadge, { children: data.memberCount })] }), _jsxs("div", { className: "flex w-full flex-wrap items-center gap-x-2.5 gap-y-1", children: [breakdown.map((item) => {
|
|
291
|
+
const Icon = item.icon;
|
|
292
|
+
return (_jsxs("span", { className: cn("inline-flex items-center gap-1 text-[10px] font-medium tabular-nums", item.className), title: `${item.count} ${item.label.toLowerCase()}`, children: [_jsx(Icon, { className: "h-3 w-3" }), item.count] }, item.key));
|
|
293
|
+
}), data.isBlocked ? (_jsx(Badge, { variant: "outline", size: "sm", className: "bg-feedback-orange-light text-feedback-orange ml-auto border-transparent", children: "Blocked" })) : null] }), _jsxs("div", { className: "text-neutral-grey-50 text-[10px]", children: [data.memberCount, " tasks"] })] }) }), _jsx(Handle, { type: "source", position: Position.Right, id: "right", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-source-right-${data.groupId}` }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-source-bottom-${data.groupId}` })] }), _jsx(PopoverContent, { className: "nodrag nopan max-h-64 w-72 overflow-y-auto p-1.5", onClick: (event) => event.stopPropagation(), children: _jsx("div", { className: "flex flex-col gap-0.5", children: data.members.map((member) => {
|
|
294
|
+
const executionDisplay = member.isBlocked
|
|
295
|
+
? null
|
|
296
|
+
: getTaskExecutionDisplayFromTask({
|
|
297
|
+
taskStatus: member.status,
|
|
298
|
+
executionState: member.executionState,
|
|
299
|
+
isBlocked: member.isBlocked,
|
|
300
|
+
});
|
|
301
|
+
const statusStyle = STATUS_STYLES[member.status];
|
|
302
|
+
const StatusIcon = statusStyle.icon;
|
|
303
|
+
return (_jsxs("button", { type: "button", className: "hover:bg-neutral-grey-5 flex w-full items-center gap-2 rounded px-2 py-2 text-left transition-colors", "data-testid": `dependency-graph-group-member-${member.taskId}`, onClick: (event) => {
|
|
304
|
+
event.stopPropagation();
|
|
305
|
+
data.onSelect(member.taskId);
|
|
306
|
+
setIsOpen(false);
|
|
307
|
+
}, children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-xs font-medium", children: member.title }), _jsxs("span", { className: "text-neutral-grey-50 flex items-center gap-1 text-[10px]", children: [member.taskKey || member.taskId.slice(0, 8).toUpperCase(), member.isOutlier ? (_jsx("span", { className: "text-feedback-blue", children: "\u2022 shown in graph" })) : null] })] }), member.isBlocked ? (_jsx(Badge, { variant: "outline", size: "sm", className: "bg-feedback-orange-light text-feedback-orange border-transparent", children: "Blocked" })) : executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, size: "sm", className: cn("max-w-28 font-bold", executionDisplay.className), children: [executionDisplay.showSpinner ? (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" })) : null, executionDisplay.label] })) : (_jsxs(Badge, { variant: "outline", size: "sm", className: cn("border-transparent font-bold", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, {}), getTaskStatusLabel(member.status)] }))] }, member.taskId));
|
|
308
|
+
}) }) })] }));
|
|
309
|
+
}
|
|
117
310
|
const nodeTypes = {
|
|
118
311
|
taskNode: TaskGraphNode,
|
|
312
|
+
taskGroupNode: TaskGroupGraphNode,
|
|
119
313
|
};
|
|
120
314
|
function getHierarchyOrthogonalPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) {
|
|
121
315
|
const sameColumn = Math.abs(sourceX - targetX) < 1;
|
|
@@ -173,7 +367,7 @@ function normalizeGraphNodeSize(size) {
|
|
|
173
367
|
function getGraphNodeSize(nodeId, nodeSizeOverrides) {
|
|
174
368
|
return (normalizeGraphNodeSize(nodeSizeOverrides?.get(nodeId)) ?? {
|
|
175
369
|
width: NODE_WIDTH,
|
|
176
|
-
height: NODE_HEIGHT,
|
|
370
|
+
height: isTaskGroupNodeId(nodeId) ? GROUP_NODE_HEIGHT : NODE_HEIGHT,
|
|
177
371
|
});
|
|
178
372
|
}
|
|
179
373
|
function getMeasuredNodeSizeOverrides(nodes) {
|
|
@@ -473,13 +667,27 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
473
667
|
const taskMap = new Map();
|
|
474
668
|
for (const t of tasks)
|
|
475
669
|
taskMap.set(t.taskId, t);
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
670
|
+
const { groups, collapsedTargetByTaskId } = collectTaskGroups(tasks, selectedTaskId, blockedTaskIds);
|
|
671
|
+
const mapTaskId = (taskId) => collapsedTargetByTaskId.get(taskId) ?? taskId;
|
|
672
|
+
const visibleTasks = tasks.filter((task) => !collapsedTargetByTaskId.has(task.taskId));
|
|
673
|
+
const childRelationshipKeys = new Set();
|
|
674
|
+
for (const task of visibleTasks) {
|
|
675
|
+
if (!task.parentTaskId)
|
|
676
|
+
continue;
|
|
677
|
+
const source = mapTaskId(task.parentTaskId);
|
|
678
|
+
if (source !== task.taskId) {
|
|
679
|
+
childRelationshipKeys.add(`${source}->${task.taskId}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (const group of groups.values()) {
|
|
683
|
+
const source = mapTaskId(group.parentTaskId);
|
|
684
|
+
if (source !== group.groupId) {
|
|
685
|
+
childRelationshipKeys.add(`${source}->${group.groupId}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
480
688
|
const dependencyHandles = getDependencyHandles(orientation);
|
|
481
689
|
const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
|
|
482
|
-
const
|
|
690
|
+
const taskNodes = visibleTasks.map((task) => ({
|
|
483
691
|
id: task.taskId,
|
|
484
692
|
type: "taskNode",
|
|
485
693
|
position: { x: 0, y: 0 },
|
|
@@ -495,7 +703,8 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
495
703
|
assigneeType: task.assigneeType ?? null,
|
|
496
704
|
assigneeDisplayName: task.assigneeDisplayName ?? null,
|
|
497
705
|
isSelected: task.taskId === selectedTaskId,
|
|
498
|
-
isBlocked: blockedTaskIds.has(task.taskId)
|
|
706
|
+
isBlocked: blockedTaskIds.has(task.taskId) ||
|
|
707
|
+
task.executionState === "WaitingForDependency",
|
|
499
708
|
orientation,
|
|
500
709
|
dependencySourceHandle: dependencyHandles.source,
|
|
501
710
|
hierarchySourceHandle: hierarchyHandles.source,
|
|
@@ -505,27 +714,33 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
505
714
|
onRemoveTask,
|
|
506
715
|
},
|
|
507
716
|
}));
|
|
717
|
+
const groupNodes = Array.from(groups.values()).map((group) => ({
|
|
718
|
+
id: group.groupId,
|
|
719
|
+
type: "taskGroupNode",
|
|
720
|
+
position: { x: 0, y: 0 },
|
|
721
|
+
data: {
|
|
722
|
+
...group,
|
|
723
|
+
orientation,
|
|
724
|
+
dependencySourceHandle: dependencyHandles.source,
|
|
725
|
+
hierarchySourceHandle: hierarchyHandles.source,
|
|
726
|
+
onSelect,
|
|
727
|
+
},
|
|
728
|
+
}));
|
|
729
|
+
const nodes = [...taskNodes, ...groupNodes];
|
|
508
730
|
const depSourceHandle = dependencyHandles.source;
|
|
509
731
|
const depTargetHandle = dependencyHandles.target;
|
|
510
732
|
const hierSourceHandle = hierarchyHandles.source;
|
|
511
733
|
const hierTargetHandle = hierarchyHandles.target;
|
|
512
734
|
const dependencyEdgeType = autoLayoutStrategy === "hierarchy" ? "step" : "smoothstep";
|
|
513
735
|
const hierarchyEdgeType = autoLayoutStrategy === "hierarchy" ? "hierarchyOrthogonal" : "smoothstep";
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
!childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
|
|
518
|
-
.map((dep) => {
|
|
519
|
-
const isBlockedBy = dep.dependencyType === "BlockedBy";
|
|
736
|
+
const buildDependencyEdge = (args) => {
|
|
737
|
+
const isBlockedBy = args.dependencyType === "BlockedBy";
|
|
738
|
+
const isSelected = args.isSelected === true;
|
|
520
739
|
const strokeColor = isBlockedBy ? EDGE_BLOCKED_BY : EDGE_RELATED;
|
|
521
|
-
const sourceTaskType = String(taskMap.get(dep.dependsOnTaskId)?.taskType ?? "").toLowerCase();
|
|
522
|
-
const isDecisionEdge = isBlockedBy && sourceTaskType === "decisionpoint";
|
|
523
|
-
const isSelected = dep.taskId === selectedTaskId &&
|
|
524
|
-
dep.dependsOnTaskId === selectedDependencyId;
|
|
525
740
|
return {
|
|
526
|
-
id:
|
|
527
|
-
source:
|
|
528
|
-
target:
|
|
741
|
+
id: args.id,
|
|
742
|
+
source: args.source,
|
|
743
|
+
target: args.target,
|
|
529
744
|
sourceHandle: depSourceHandle,
|
|
530
745
|
targetHandle: depTargetHandle,
|
|
531
746
|
type: dependencyEdgeType,
|
|
@@ -533,7 +748,7 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
533
748
|
style: {
|
|
534
749
|
stroke: isSelected ? "var(--color-neutral-grey-100)" : strokeColor,
|
|
535
750
|
strokeWidth: isSelected ? 3 : isBlockedBy ? 2 : 1.5,
|
|
536
|
-
strokeDasharray: isDecisionEdge ? "6 4" : undefined,
|
|
751
|
+
strokeDasharray: args.isDecisionEdge ? "6 4" : undefined,
|
|
537
752
|
},
|
|
538
753
|
markerEnd: {
|
|
539
754
|
type: MarkerType.ArrowClosed,
|
|
@@ -541,28 +756,88 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
541
756
|
width: 16,
|
|
542
757
|
height: 16,
|
|
543
758
|
},
|
|
544
|
-
label:
|
|
545
|
-
?
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
759
|
+
label: args.count && args.count > 1
|
|
760
|
+
? `×${args.count}`
|
|
761
|
+
: isBlockedBy
|
|
762
|
+
? args.isDecisionEdge
|
|
763
|
+
? "decision"
|
|
764
|
+
: undefined
|
|
765
|
+
: "related",
|
|
549
766
|
labelStyle: {
|
|
550
767
|
fontSize: 10,
|
|
551
|
-
fill: isDecisionEdge ? EDGE_BLOCKED_BY : EDGE_RELATED,
|
|
768
|
+
fill: args.isDecisionEdge ? EDGE_BLOCKED_BY : EDGE_RELATED,
|
|
552
769
|
},
|
|
553
770
|
data: {
|
|
554
771
|
relationship: "dependency",
|
|
555
|
-
|
|
556
|
-
|
|
772
|
+
dependencyType: args.dependencyType,
|
|
773
|
+
count: args.count,
|
|
774
|
+
taskId: args.taskId,
|
|
775
|
+
dependsOnTaskId: args.dependsOnTaskId,
|
|
557
776
|
},
|
|
558
|
-
selectable:
|
|
777
|
+
selectable: args.selectable,
|
|
559
778
|
};
|
|
560
|
-
}
|
|
779
|
+
};
|
|
780
|
+
const dependencyEdges = [];
|
|
781
|
+
const syntheticDependencies = new Map();
|
|
782
|
+
for (const dep of dependencies) {
|
|
783
|
+
if (!taskMap.has(dep.taskId) || !taskMap.has(dep.dependsOnTaskId))
|
|
784
|
+
continue;
|
|
785
|
+
const source = mapTaskId(dep.dependsOnTaskId);
|
|
786
|
+
const target = mapTaskId(dep.taskId);
|
|
787
|
+
if (source === target ||
|
|
788
|
+
childRelationshipKeys.has(`${source}->${target}`)) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const isBlockedBy = dep.dependencyType === "BlockedBy";
|
|
792
|
+
const sourceTaskType = String(taskMap.get(dep.dependsOnTaskId)?.taskType ?? "").toLowerCase();
|
|
793
|
+
const isDecisionEdge = isBlockedBy && sourceTaskType === "decisionpoint";
|
|
794
|
+
const wasRemapped = source !== dep.dependsOnTaskId || target !== dep.taskId;
|
|
795
|
+
if (!wasRemapped) {
|
|
796
|
+
dependencyEdges.push(buildDependencyEdge({
|
|
797
|
+
id: dep.dependencyId,
|
|
798
|
+
source,
|
|
799
|
+
target,
|
|
800
|
+
dependencyType: dep.dependencyType,
|
|
801
|
+
isDecisionEdge,
|
|
802
|
+
isSelected: dep.taskId === selectedTaskId &&
|
|
803
|
+
dep.dependsOnTaskId === selectedDependencyId,
|
|
804
|
+
taskId: dep.taskId,
|
|
805
|
+
dependsOnTaskId: dep.dependsOnTaskId,
|
|
806
|
+
selectable: true,
|
|
807
|
+
}));
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const key = `${source}->${target}`;
|
|
811
|
+
const existing = syntheticDependencies.get(key);
|
|
812
|
+
if (existing) {
|
|
813
|
+
existing.count += 1;
|
|
814
|
+
if (isBlockedBy)
|
|
815
|
+
existing.dependencyType = "BlockedBy";
|
|
816
|
+
if (isDecisionEdge)
|
|
817
|
+
existing.isDecisionEdge = true;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
syntheticDependencies.set(key, {
|
|
821
|
+
source,
|
|
822
|
+
target,
|
|
823
|
+
dependencyType: isBlockedBy ? "BlockedBy" : "RelatedTo",
|
|
824
|
+
isDecisionEdge,
|
|
825
|
+
count: 1,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const [key, summary] of Array.from(syntheticDependencies.entries()).sort(([a], [b]) => a.localeCompare(b))) {
|
|
830
|
+
dependencyEdges.push(buildDependencyEdge({
|
|
831
|
+
id: `groupdep:${key}`,
|
|
832
|
+
...summary,
|
|
833
|
+
selectable: false,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
561
836
|
const hierarchyEdges = visibleTasks
|
|
562
837
|
.filter((task) => !!task.parentTaskId && taskMap.has(task.parentTaskId))
|
|
563
838
|
.map((task) => ({
|
|
564
839
|
id: `parent:${task.parentTaskId}:${task.taskId}`,
|
|
565
|
-
source: task.parentTaskId,
|
|
840
|
+
source: mapTaskId(task.parentTaskId),
|
|
566
841
|
target: task.taskId,
|
|
567
842
|
sourceHandle: hierSourceHandle,
|
|
568
843
|
targetHandle: hierTargetHandle,
|
|
@@ -581,13 +856,35 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
|
|
|
581
856
|
relationship: "hierarchy",
|
|
582
857
|
},
|
|
583
858
|
}));
|
|
859
|
+
for (const group of groups.values()) {
|
|
860
|
+
hierarchyEdges.push({
|
|
861
|
+
id: `parent:${group.parentTaskId}:${group.groupId}`,
|
|
862
|
+
source: mapTaskId(group.parentTaskId),
|
|
863
|
+
target: group.groupId,
|
|
864
|
+
sourceHandle: hierSourceHandle,
|
|
865
|
+
targetHandle: hierTargetHandle,
|
|
866
|
+
type: hierarchyEdgeType,
|
|
867
|
+
style: {
|
|
868
|
+
stroke: "var(--color-component-default)",
|
|
869
|
+
strokeWidth: 1.5,
|
|
870
|
+
},
|
|
871
|
+
markerEnd: {
|
|
872
|
+
type: MarkerType.ArrowClosed,
|
|
873
|
+
color: "var(--color-component-default)",
|
|
874
|
+
width: 16,
|
|
875
|
+
height: 16,
|
|
876
|
+
},
|
|
877
|
+
data: { relationship: "hierarchy" },
|
|
878
|
+
});
|
|
879
|
+
}
|
|
584
880
|
const edges = [...dependencyEdges, ...hierarchyEdges];
|
|
585
881
|
if (nodes.length === 0)
|
|
586
882
|
return { nodes: [], edges: [] };
|
|
587
883
|
return layoutGraph(nodes, edges, orientation, autoLayoutStrategy, nodeSizeOverrides);
|
|
588
884
|
}
|
|
589
885
|
export function buildDependencyGraphLayout(args) {
|
|
590
|
-
|
|
886
|
+
const blockedTaskIds = collectBlockedTaskIds(args.tasks, args.dependencies);
|
|
887
|
+
return buildGraph(args.tasks, args.dependencies, args.selectedTaskId ?? null, null, blockedTaskIds, args.orientation ?? "horizontal", false, args.autoLayoutStrategy ?? "hierarchy", () => undefined);
|
|
591
888
|
}
|
|
592
889
|
function applySavedLayout(nodes, savedLayout) {
|
|
593
890
|
if (!savedLayout?.nodes?.length) {
|
|
@@ -600,10 +897,30 @@ function applySavedLayout(nodes, savedLayout) {
|
|
|
600
897
|
y: node.y,
|
|
601
898
|
},
|
|
602
899
|
]));
|
|
603
|
-
return nodes.map((node) =>
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
900
|
+
return nodes.map((node) => {
|
|
901
|
+
if (node.type === "taskGroupNode") {
|
|
902
|
+
const memberPositions = node.data.members
|
|
903
|
+
.filter((member) => !member.isOutlier)
|
|
904
|
+
.map((member) => positionsByTaskId.get(member.taskId))
|
|
905
|
+
.filter((position) => !!position);
|
|
906
|
+
if (memberPositions.length > 0) {
|
|
907
|
+
return {
|
|
908
|
+
...node,
|
|
909
|
+
position: {
|
|
910
|
+
x: memberPositions.reduce((sum, position) => sum + position.x, 0) /
|
|
911
|
+
memberPositions.length,
|
|
912
|
+
y: memberPositions.reduce((sum, position) => sum + position.y, 0) /
|
|
913
|
+
memberPositions.length,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return node;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
...node,
|
|
921
|
+
position: positionsByTaskId.get(node.id) ?? node.position,
|
|
922
|
+
};
|
|
923
|
+
});
|
|
607
924
|
}
|
|
608
925
|
function preserveNodePositions(previousNodes, nextNodes) {
|
|
609
926
|
const previousPositions = new Map(previousNodes.map((node) => [node.id, node.position]));
|
|
@@ -716,17 +1033,11 @@ function isGraphConnectionValid(connection, tasks, dependencies, orientation, au
|
|
|
716
1033
|
}
|
|
717
1034
|
return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
|
|
718
1035
|
}
|
|
719
|
-
function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
|
|
1036
|
+
function createLayoutSignature(layoutKey, nodes, viewport, orientation, savedLayout, collapsedMemberIds = new Set()) {
|
|
720
1037
|
return JSON.stringify({
|
|
721
1038
|
layoutKey,
|
|
722
1039
|
orientation,
|
|
723
|
-
nodes: nodes
|
|
724
|
-
.map((node) => ({
|
|
725
|
-
taskId: node.id,
|
|
726
|
-
x: node.position.x,
|
|
727
|
-
y: node.position.y,
|
|
728
|
-
}))
|
|
729
|
-
.sort((a, b) => a.taskId.localeCompare(b.taskId)),
|
|
1040
|
+
nodes: buildPersistableLayoutNodes(nodes, savedLayout, collapsedMemberIds).sort((a, b) => a.taskId.localeCompare(b.taskId)),
|
|
730
1041
|
viewport: viewport
|
|
731
1042
|
? {
|
|
732
1043
|
x: viewport.x,
|
|
@@ -736,22 +1047,76 @@ function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
|
|
|
736
1047
|
: null,
|
|
737
1048
|
});
|
|
738
1049
|
}
|
|
1050
|
+
function buildPersistableLayoutNodes(nodes, savedLayout, collapsedMemberIds) {
|
|
1051
|
+
const renderedTaskIds = new Set();
|
|
1052
|
+
const persistableNodes = [];
|
|
1053
|
+
for (const node of nodes) {
|
|
1054
|
+
if (node.type === "taskGroupNode")
|
|
1055
|
+
continue;
|
|
1056
|
+
renderedTaskIds.add(node.id);
|
|
1057
|
+
persistableNodes.push({
|
|
1058
|
+
taskId: node.id,
|
|
1059
|
+
x: node.position.x,
|
|
1060
|
+
y: node.position.y,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
for (const savedNode of savedLayout?.nodes ?? []) {
|
|
1064
|
+
if (collapsedMemberIds.has(savedNode.taskId) &&
|
|
1065
|
+
!renderedTaskIds.has(savedNode.taskId)) {
|
|
1066
|
+
persistableNodes.push({
|
|
1067
|
+
taskId: savedNode.taskId,
|
|
1068
|
+
x: savedNode.x,
|
|
1069
|
+
y: savedNode.y,
|
|
1070
|
+
});
|
|
1071
|
+
renderedTaskIds.add(savedNode.taskId);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return persistableNodes;
|
|
1075
|
+
}
|
|
739
1076
|
function createGraphSyncSignature(nodes, edges) {
|
|
740
1077
|
return JSON.stringify({
|
|
741
1078
|
nodes: nodes
|
|
742
|
-
.map((node) =>
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1079
|
+
.map((node) => {
|
|
1080
|
+
if (node.type === "taskGroupNode") {
|
|
1081
|
+
return {
|
|
1082
|
+
id: node.id,
|
|
1083
|
+
type: node.type,
|
|
1084
|
+
x: node.position.x,
|
|
1085
|
+
y: node.position.y,
|
|
1086
|
+
memberCount: node.data.memberCount,
|
|
1087
|
+
statusCounts: node.data.statusCounts,
|
|
1088
|
+
label: node.data.label,
|
|
1089
|
+
status: node.data.status,
|
|
1090
|
+
blocked: node.data.isBlocked,
|
|
1091
|
+
members: node.data.members
|
|
1092
|
+
.map((member) => ({
|
|
1093
|
+
taskId: member.taskId,
|
|
1094
|
+
taskKey: member.taskKey,
|
|
1095
|
+
title: member.title,
|
|
1096
|
+
status: member.status,
|
|
1097
|
+
executionState: member.executionState ?? null,
|
|
1098
|
+
isBlocked: member.isBlocked,
|
|
1099
|
+
isOutlier: member.isOutlier,
|
|
1100
|
+
}))
|
|
1101
|
+
.sort((a, b) => a.taskId.localeCompare(b.taskId)),
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
id: node.id,
|
|
1106
|
+
type: node.type,
|
|
1107
|
+
x: node.position.x,
|
|
1108
|
+
y: node.position.y,
|
|
1109
|
+
title: node.data.title,
|
|
1110
|
+
selected: node.data.isSelected,
|
|
1111
|
+
status: node.data.status,
|
|
1112
|
+
executionState: node.data.executionState ?? null,
|
|
1113
|
+
priority: node.data.priority ?? null,
|
|
1114
|
+
blocked: node.data.isBlocked,
|
|
1115
|
+
taskType: node.data.taskType ?? null,
|
|
1116
|
+
assigneeType: node.data.assigneeType ?? null,
|
|
1117
|
+
assignee: node.data.assigneeDisplayName ?? null,
|
|
1118
|
+
};
|
|
1119
|
+
})
|
|
755
1120
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
756
1121
|
edges: edges
|
|
757
1122
|
.map((edge) => ({
|
|
@@ -760,6 +1125,9 @@ function createGraphSyncSignature(nodes, edges) {
|
|
|
760
1125
|
target: edge.target,
|
|
761
1126
|
sourceHandle: edge.sourceHandle ?? null,
|
|
762
1127
|
targetHandle: edge.targetHandle ?? null,
|
|
1128
|
+
relationship: edge.data?.relationship ?? null,
|
|
1129
|
+
dependencyType: edge.data?.dependencyType ?? null,
|
|
1130
|
+
count: edge.data?.count ?? null,
|
|
763
1131
|
}))
|
|
764
1132
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
765
1133
|
});
|
|
@@ -776,33 +1144,12 @@ export function DependencyGraphView(props) {
|
|
|
776
1144
|
: null;
|
|
777
1145
|
const [transientOrientation, setTransientOrientation] = useState(null);
|
|
778
1146
|
const effectiveOrientation = transientOrientation ?? savedLayoutOrientation ?? fallbackOrientation;
|
|
779
|
-
const taskMap = useMemo(() => {
|
|
780
|
-
const m = new Map();
|
|
781
|
-
for (const t of tasks)
|
|
782
|
-
m.set(t.taskId, t);
|
|
783
|
-
return m;
|
|
784
|
-
}, [tasks]);
|
|
785
1147
|
const blockedTaskIds = useMemo(() => {
|
|
786
1148
|
if (!highlightBlockedTasks) {
|
|
787
1149
|
return new Set();
|
|
788
1150
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if ((task.executionBlockedByProjectIds?.length ?? 0) > 0) {
|
|
792
|
-
blocked.add(task.taskId);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
for (const dep of dependencies) {
|
|
796
|
-
if (dep.dependencyType !== "BlockedBy")
|
|
797
|
-
continue;
|
|
798
|
-
const blocker = taskMap.get(dep.dependsOnTaskId);
|
|
799
|
-
if (!blocker ||
|
|
800
|
-
normalizeTaskStatus(blocker.status, blocker.executionState) !== "Done") {
|
|
801
|
-
blocked.add(dep.taskId);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
return blocked;
|
|
805
|
-
}, [dependencies, highlightBlockedTasks, taskMap]);
|
|
1151
|
+
return collectBlockedTaskIds(tasks, dependencies);
|
|
1152
|
+
}, [dependencies, highlightBlockedTasks, tasks]);
|
|
806
1153
|
const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
|
|
807
1154
|
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
|
|
808
1155
|
tasks,
|
|
@@ -818,6 +1165,18 @@ export function DependencyGraphView(props) {
|
|
|
818
1165
|
onAddChildTaskFromNode,
|
|
819
1166
|
onRemoveTask,
|
|
820
1167
|
]);
|
|
1168
|
+
const collapsedMemberIds = useMemo(() => {
|
|
1169
|
+
const ids = new Set();
|
|
1170
|
+
for (const node of initialNodes) {
|
|
1171
|
+
if (node.type !== "taskGroupNode")
|
|
1172
|
+
continue;
|
|
1173
|
+
for (const member of node.data.members) {
|
|
1174
|
+
if (!member.isOutlier)
|
|
1175
|
+
ids.add(member.taskId);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return ids;
|
|
1179
|
+
}, [initialNodes]);
|
|
821
1180
|
const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
|
|
822
1181
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
823
1182
|
const nodesRef = useRef(nodes);
|
|
@@ -959,17 +1318,13 @@ export function DependencyGraphView(props) {
|
|
|
959
1318
|
if (!canPersistLayout) {
|
|
960
1319
|
return;
|
|
961
1320
|
}
|
|
962
|
-
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
|
|
1321
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation, savedLayout, collapsedMemberIds);
|
|
963
1322
|
if (signature === lastSavedSignatureRef.current) {
|
|
964
1323
|
return;
|
|
965
1324
|
}
|
|
966
1325
|
try {
|
|
967
1326
|
const layoutPayload = {
|
|
968
|
-
nodes: nextNodes
|
|
969
|
-
taskId: node.id,
|
|
970
|
-
x: node.position.x,
|
|
971
|
-
y: node.position.y,
|
|
972
|
-
})),
|
|
1327
|
+
nodes: buildPersistableLayoutNodes(nextNodes, savedLayout, collapsedMemberIds),
|
|
973
1328
|
orientation: nextOrientation,
|
|
974
1329
|
viewport: viewport == null
|
|
975
1330
|
? null
|
|
@@ -1015,17 +1370,18 @@ export function DependencyGraphView(props) {
|
|
|
1015
1370
|
}
|
|
1016
1371
|
}, [
|
|
1017
1372
|
canPersistLayout,
|
|
1373
|
+
collapsedMemberIds,
|
|
1018
1374
|
effectiveLayoutKey,
|
|
1019
|
-
effectiveOrientation,
|
|
1020
1375
|
onLayoutSaved,
|
|
1021
1376
|
onPersistLayout,
|
|
1022
1377
|
projectId,
|
|
1378
|
+
savedLayout,
|
|
1023
1379
|
]);
|
|
1024
1380
|
const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
|
|
1025
1381
|
if (!canPersistLayout) {
|
|
1026
1382
|
return;
|
|
1027
1383
|
}
|
|
1028
|
-
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
|
|
1384
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation, savedLayout, collapsedMemberIds);
|
|
1029
1385
|
if (signature === lastSavedSignatureRef.current) {
|
|
1030
1386
|
return;
|
|
1031
1387
|
}
|
|
@@ -1043,10 +1399,12 @@ export function DependencyGraphView(props) {
|
|
|
1043
1399
|
}, layoutSaveDebounceMs);
|
|
1044
1400
|
}, [
|
|
1045
1401
|
canPersistLayout,
|
|
1402
|
+
collapsedMemberIds,
|
|
1046
1403
|
effectiveLayoutKey,
|
|
1047
1404
|
effectiveOrientation,
|
|
1048
1405
|
layoutSaveDebounceMs,
|
|
1049
1406
|
persistLayout,
|
|
1407
|
+
savedLayout,
|
|
1050
1408
|
]);
|
|
1051
1409
|
const restoreViewport = useCallback(() => {
|
|
1052
1410
|
const instance = reactFlowRef.current;
|
|
@@ -1059,7 +1417,7 @@ export function DependencyGraphView(props) {
|
|
|
1059
1417
|
skipNextMoveEndSaveRef.current = true;
|
|
1060
1418
|
window.requestAnimationFrame(() => {
|
|
1061
1419
|
reactFlowRef.current?.setViewport(viewport, { duration: 0 });
|
|
1062
|
-
lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
|
|
1420
|
+
lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation, savedLayout, collapsedMemberIds);
|
|
1063
1421
|
});
|
|
1064
1422
|
return;
|
|
1065
1423
|
}
|
|
@@ -1078,14 +1436,21 @@ export function DependencyGraphView(props) {
|
|
|
1078
1436
|
const viewport = reactFlowRef.current?.getViewport() ?? null;
|
|
1079
1437
|
queueLayoutSave(nodesRef.current, viewport, nextOrientation);
|
|
1080
1438
|
});
|
|
1081
|
-
}, [
|
|
1439
|
+
}, [
|
|
1440
|
+
collapsedMemberIds,
|
|
1441
|
+
effectiveLayoutKey,
|
|
1442
|
+
effectiveOrientation,
|
|
1443
|
+
queueLayoutSave,
|
|
1444
|
+
savedLayout,
|
|
1445
|
+
]);
|
|
1082
1446
|
useEffect(() => {
|
|
1083
1447
|
const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
|
|
1084
1448
|
lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
|
|
1085
1449
|
const nextNodeIdSignature = initialNodes.map((node) => node.id).join("|");
|
|
1086
|
-
const
|
|
1450
|
+
const savedLayoutTaskIds = new Set((savedLayout?.nodes ?? []).map((node) => node.taskId));
|
|
1451
|
+
const hasRenderedNodeMissingFromSavedLayout = initialNodes.some((node) => node.type !== "taskGroupNode" && !savedLayoutTaskIds.has(node.id));
|
|
1087
1452
|
const shouldAutoLayoutForHierarchy = autoLayoutStrategy === "hierarchy" &&
|
|
1088
|
-
(
|
|
1453
|
+
(hasRenderedNodeMissingFromSavedLayout ||
|
|
1089
1454
|
(lastAppliedNodeIdSignatureRef.current !== null &&
|
|
1090
1455
|
lastAppliedNodeIdSignatureRef.current !== nextNodeIdSignature));
|
|
1091
1456
|
const positionedNodes = shouldApplySavedLayout
|
|
@@ -1117,7 +1482,7 @@ export function DependencyGraphView(props) {
|
|
|
1117
1482
|
shouldAutoLayoutForHierarchy || !savedLayout?.viewport;
|
|
1118
1483
|
if (!savedLayout?.viewport || shouldAutoLayoutForHierarchy) {
|
|
1119
1484
|
lastSavedSignatureRef.current = savedLayout?.nodes?.length
|
|
1120
|
-
? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
|
|
1485
|
+
? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation, savedLayout, collapsedMemberIds)
|
|
1121
1486
|
: null;
|
|
1122
1487
|
}
|
|
1123
1488
|
if (shouldAutoLayoutForHierarchy) {
|
|
@@ -1134,6 +1499,7 @@ export function DependencyGraphView(props) {
|
|
|
1134
1499
|
}
|
|
1135
1500
|
}, [
|
|
1136
1501
|
autoLayoutStrategy,
|
|
1502
|
+
collapsedMemberIds,
|
|
1137
1503
|
getLatestGraphNodes,
|
|
1138
1504
|
initialEdges,
|
|
1139
1505
|
initialNodes,
|
|
@@ -1305,6 +1671,9 @@ export function DependencyGraphView(props) {
|
|
|
1305
1671
|
}
|
|
1306
1672
|
}, onNodeClick: (_event, node) => {
|
|
1307
1673
|
focusGraphContainer();
|
|
1674
|
+
if (node.type === "taskGroupNode") {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1308
1677
|
if (performance.now() < suppressNodeSelectionUntilRef.current) {
|
|
1309
1678
|
return;
|
|
1310
1679
|
}
|
|
@@ -1312,6 +1681,9 @@ export function DependencyGraphView(props) {
|
|
|
1312
1681
|
onSelectTask(node.id);
|
|
1313
1682
|
}, onNodeDragStart: (_event, node) => {
|
|
1314
1683
|
focusGraphContainer();
|
|
1684
|
+
if (node.type === "taskGroupNode") {
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1315
1687
|
if (performance.now() < suppressNodeSelectionUntilRef.current) {
|
|
1316
1688
|
return;
|
|
1317
1689
|
}
|