@parhelia/core 0.1.12368 → 0.1.12390
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 +1 -1
- package/dist/agents-view/AgentsView.js.map +1 -1
- package/dist/components/ui/LanguageSelector.js +1 -3
- package/dist/components/ui/LanguageSelector.js.map +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/config/config.js +53 -16
- package/dist/config/config.js.map +1 -1
- package/dist/config/notificationRoutes.js +10 -0
- package/dist/config/notificationRoutes.js.map +1 -1
- package/dist/config/types/workspace.d.ts +6 -0
- package/dist/config/types.d.ts +2 -5
- package/dist/editor/Editor.js +11 -4
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/ai/AgentCostDisplay.d.ts +1 -0
- package/dist/editor/ai/AgentCostDisplay.js +1 -1
- package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +144 -36
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/AgentTerminalStatusBar.d.ts +2 -0
- package/dist/editor/ai/AgentTerminalStatusBar.js +22 -37
- package/dist/editor/ai/AgentTerminalStatusBar.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.js +0 -1
- package/dist/editor/ai/AiResponseMessage.js.map +1 -1
- package/dist/editor/ai/ContentInspectorPopover.d.ts +17 -0
- package/dist/editor/ai/ContentInspectorPopover.js +136 -0
- package/dist/editor/ai/ContentInspectorPopover.js.map +1 -0
- package/dist/editor/ai/ContextInfoBar.js +55 -2
- package/dist/editor/ai/ContextInfoBar.js.map +1 -1
- package/dist/editor/ai/InlineAiDialog.js +1 -7
- package/dist/editor/ai/InlineAiDialog.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.d.ts +4 -0
- package/dist/editor/ai/ToolCallDisplay.js +43 -8
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/ai/dialogs/AgentDialogHandler.js +46 -22
- package/dist/editor/ai/dialogs/AgentDialogHandler.js.map +1 -1
- package/dist/editor/client/EditorShell.js +69 -26
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +3 -2
- package/dist/editor/client/hooks/useQuota.d.ts +2 -1
- package/dist/editor/client/hooks/useQuota.js.map +1 -1
- package/dist/editor/client/hooks/useSocketMessageHandler.js +28 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
- package/dist/editor/client/operations.d.ts +1 -0
- package/dist/editor/client/operations.js +67 -15
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/client/waitForEditOperationTerminal.d.ts +11 -0
- package/dist/editor/client/waitForEditOperationTerminal.js +40 -0
- package/dist/editor/client/waitForEditOperationTerminal.js.map +1 -0
- package/dist/editor/commands/commands.d.ts +11 -1
- package/dist/editor/commands/commands.js +12 -1
- package/dist/editor/commands/commands.js.map +1 -1
- package/dist/editor/commands/customCommandConverter.d.ts +8 -1
- package/dist/editor/commands/customCommandConverter.js +33 -4
- package/dist/editor/commands/customCommandConverter.js.map +1 -1
- package/dist/editor/commands/handlers/uiActionHandlers.d.ts +6 -0
- package/dist/editor/commands/handlers/uiActionHandlers.js +84 -0
- package/dist/editor/commands/handlers/uiActionHandlers.js.map +1 -0
- package/dist/editor/commands/itemCommands.js +6 -2
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/commands/keyboardCommands.d.ts +10 -0
- package/dist/editor/commands/keyboardCommands.js +142 -0
- package/dist/editor/commands/keyboardCommands.js.map +1 -0
- package/dist/editor/commands/undo.d.ts +9 -15
- package/dist/editor/commands/undo.js +24 -0
- package/dist/editor/commands/undo.js.map +1 -1
- package/dist/editor/menubar/PageSelector.js +1 -3
- package/dist/editor/menubar/PageSelector.js.map +1 -1
- package/dist/editor/menubar/VersionSelector.js +1 -3
- package/dist/editor/menubar/VersionSelector.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js +7 -36
- package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js.map +1 -1
- package/dist/editor/notifications/notificationRoutes.js +1 -0
- package/dist/editor/notifications/notificationRoutes.js.map +1 -1
- package/dist/editor/page-editor-chrome/InlineEditor.js +53 -36
- package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
- package/dist/editor/page-viewer/PageViewer.js +60 -6
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/reviews/Comment.js +12 -10
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentDisplayPopover.js +1 -3
- package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
- package/dist/editor/reviews/PreviewInfo.js +1 -4
- package/dist/editor/reviews/PreviewInfo.js.map +1 -1
- package/dist/editor/reviews/reviewCommands.js +4 -1
- package/dist/editor/reviews/reviewCommands.js.map +1 -1
- package/dist/editor/reviews/useReviews.d.ts +2 -2
- package/dist/editor/reviews/useReviews.js +12 -30
- package/dist/editor/reviews/useReviews.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +26 -0
- package/dist/editor/services/agentService.js +41 -0
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +7 -1
- package/dist/editor/services/aiService.js +13 -1
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/editor/services/notificationService.d.ts +1 -0
- package/dist/editor/services/notificationService.js +1 -0
- package/dist/editor/services/notificationService.js.map +1 -1
- package/dist/editor/services/reviewsService.d.ts +2 -5
- package/dist/editor/services/reviewsService.js +0 -10
- package/dist/editor/services/reviewsService.js.map +1 -1
- package/dist/editor/services/systemService.d.ts +2 -1
- package/dist/editor/services/systemService.js +3 -0
- package/dist/editor/services/systemService.js.map +1 -1
- package/dist/editor/settings/QuotaInfo.js +15 -7
- package/dist/editor/settings/QuotaInfo.js.map +1 -1
- package/dist/editor/settings/index/useIndexStatus.js +1 -1
- package/dist/editor/settings/index/useIndexStatus.js.map +1 -1
- package/dist/editor/settings/panels/AgentProfileConfigPanel.d.ts +10 -0
- package/dist/editor/settings/panels/AgentProfileConfigPanel.js +61 -0
- package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -0
- package/dist/editor/settings/panels/AgentsPanel.d.ts +0 -4
- package/dist/editor/settings/panels/AgentsPanel.js +101 -109
- package/dist/editor/settings/panels/AgentsPanel.js.map +1 -1
- package/dist/editor/settings/panels/CreateAgentProfileDialog.d.ts +7 -0
- package/dist/editor/settings/panels/CreateAgentProfileDialog.js +48 -0
- package/dist/editor/settings/panels/CreateAgentProfileDialog.js.map +1 -0
- package/dist/editor/settings/panels/GroupedFieldConfigPanel.d.ts +33 -0
- package/dist/editor/settings/panels/GroupedFieldConfigPanel.js +91 -0
- package/dist/editor/settings/panels/GroupedFieldConfigPanel.js.map +1 -0
- package/dist/editor/settings/panels/ModelConfigPanel.d.ts +10 -0
- package/dist/editor/settings/panels/ModelConfigPanel.js +51 -0
- package/dist/editor/settings/panels/ModelConfigPanel.js.map +1 -0
- package/dist/editor/settings/panels/ModelsPanel.js +201 -70
- package/dist/editor/settings/panels/ModelsPanel.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +10 -0
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +46 -0
- package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -0
- package/dist/editor/settings/panels/ProjectTemplatesPanel.d.ts +2 -0
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js +1340 -0
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -0
- package/dist/editor/settings/panels/ProviderConfigPanel.d.ts +10 -0
- package/dist/editor/settings/panels/ProviderConfigPanel.js +32 -0
- package/dist/editor/settings/panels/ProviderConfigPanel.js.map +1 -0
- package/dist/editor/settings/panels/ProvidersPanel.js +46 -4
- package/dist/editor/settings/panels/ProvidersPanel.js.map +1 -1
- package/dist/editor/settings/panels/SearchConfigPanel.js +3 -3
- package/dist/editor/settings/panels/SearchConfigPanel.js.map +1 -1
- package/dist/editor/settings/panels/index.d.ts +1 -2
- package/dist/editor/settings/panels/index.js +1 -2
- package/dist/editor/settings/panels/index.js.map +1 -1
- package/dist/editor/ui/ItemNameDialogNew.js +15 -9
- package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
- package/dist/editor/utils/keyboardNavigation.d.ts +6 -20
- package/dist/editor/utils/keyboardNavigation.js +48 -139
- package/dist/editor/utils/keyboardNavigation.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/setup/services/setupWizardService.d.ts +8 -10
- package/dist/setup/services/setupWizardService.js +4 -17
- package/dist/setup/services/setupWizardService.js.map +1 -1
- package/dist/splash-screen/ModernSplashScreen.js +100 -18
- package/dist/splash-screen/ModernSplashScreen.js.map +1 -1
- package/dist/splash-screen/ParheliaAssistantChat.js +3 -22
- package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -1
- package/dist/task-board/TaskBoardWorkspace.js +70 -3
- package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
- package/dist/task-board/components/AssignAgentDialog.js +9 -4
- package/dist/task-board/components/AssignAgentDialog.js.map +1 -1
- package/dist/task-board/components/CreateProjectDialog.js +32 -34
- package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
- package/dist/task-board/components/CreateTaskDialog.d.ts +2 -0
- package/dist/task-board/components/CreateTaskDialog.js +35 -11
- package/dist/task-board/components/CreateTaskDialog.js.map +1 -1
- package/dist/task-board/components/ProjectPropertiesPanel.js +4 -1
- package/dist/task-board/components/ProjectPropertiesPanel.js.map +1 -1
- package/dist/task-board/components/TaskAgentPanel.js +13 -4
- package/dist/task-board/components/TaskAgentPanel.js.map +1 -1
- package/dist/task-board/components/TaskAssigneePicker.d.ts +2 -2
- package/dist/task-board/components/TaskAssigneePicker.js +12 -4
- package/dist/task-board/components/TaskAssigneePicker.js.map +1 -1
- package/dist/task-board/components/TaskBoardProjectListSidebar.js.map +1 -1
- package/dist/task-board/components/TaskCard.js +2 -2
- package/dist/task-board/components/TaskCard.js.map +1 -1
- package/dist/task-board/components/TaskDetailPanel.js +2 -2
- package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
- package/dist/task-board/components/TaskRow.js +2 -2
- package/dist/task-board/components/TaskRow.js.map +1 -1
- package/dist/task-board/components/WizardCommunicationCenter.js +10 -4
- package/dist/task-board/components/WizardCommunicationCenter.js.map +1 -1
- package/dist/task-board/services/taskService.d.ts +11 -2
- package/dist/task-board/services/taskService.js +20 -2
- package/dist/task-board/services/taskService.js.map +1 -1
- package/dist/task-board/types.d.ts +52 -7
- package/dist/task-board/views/DependencyGraphView.d.ts +31 -4
- package/dist/task-board/views/DependencyGraphView.js +383 -64
- package/dist/task-board/views/DependencyGraphView.js.map +1 -1
- package/dist/types.d.ts +23 -15
- package/package.json +7 -7
- package/dist/editor/settings/Setup.d.ts +0 -1
- package/dist/editor/settings/Setup.js +0 -211
- package/dist/editor/settings/Setup.js.map +0 -1
- package/dist/editor/settings/panels/DatabasePanel.d.ts +0 -6
- package/dist/editor/settings/panels/DatabasePanel.js +0 -50
- package/dist/editor/settings/panels/DatabasePanel.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.d.ts +0 -2
- package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js +0 -195
- package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/index.d.ts +0 -2
- package/dist/editor/settings/setup-steps/AiSetupStep/index.js +0 -21
- package/dist/editor/settings/setup-steps/AiSetupStep/index.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.d.ts +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js +0 -233
- package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.d.ts +0 -15
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js +0 -14
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.d.ts +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js +0 -94
- package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/types.d.ts +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/types.js +0 -2
- package/dist/editor/settings/setup-steps/AiSetupStep/types.js.map +0 -1
- package/dist/editor/settings/setup-steps/AiSetupStep/utils.d.ts +0 -5
- package/dist/editor/settings/setup-steps/AiSetupStep/utils.js +0 -44
- package/dist/editor/settings/setup-steps/AiSetupStep/utils.js.map +0 -1
- package/dist/editor/settings/setup-steps/IndexSetupStep.d.ts +0 -2
- package/dist/editor/settings/setup-steps/IndexSetupStep.js +0 -36
- package/dist/editor/settings/setup-steps/IndexSetupStep.js.map +0 -1
- package/dist/editor/settings/setup-steps/SettingsSetupStep.d.ts +0 -2
- package/dist/editor/settings/setup-steps/SettingsSetupStep.js +0 -111
- package/dist/editor/settings/setup-steps/SettingsSetupStep.js.map +0 -1
- package/dist/editor/settings/setup-steps/SetupOverview.d.ts +0 -14
- package/dist/editor/settings/setup-steps/SetupOverview.js +0 -38
- package/dist/editor/settings/setup-steps/SetupOverview.js.map +0 -1
|
@@ -5,13 +5,19 @@ import Dagre from "@dagrejs/dagre";
|
|
|
5
5
|
import "@xyflow/react/dist/style.css";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
import { normalizeTaskStatus, getTaskStatusLabel } from "../taskStatus";
|
|
8
|
+
import { getTaskExecutionDisplayFromTask } from "../taskExecutionStatus";
|
|
8
9
|
import { cn } from "../../lib/utils";
|
|
9
10
|
import { saveGraphLayout } from "../services/taskService";
|
|
10
11
|
import { Button } from "../../components/ui/button";
|
|
11
|
-
import {
|
|
12
|
+
import { Badge } from "../../components/ui/badge";
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip";
|
|
14
|
+
import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, Loader2, Plus, Trash2, } from "lucide-react";
|
|
12
15
|
const NODE_WIDTH = 220;
|
|
13
16
|
const NODE_HEIGHT = 80;
|
|
14
17
|
const SAVE_DEBOUNCE_MS = 500;
|
|
18
|
+
const EDGE_BLOCKED_BY = "#f97316";
|
|
19
|
+
const EDGE_CHILD_TASK = "#0d9488";
|
|
20
|
+
const EDGE_RELATED = "#94a3b8";
|
|
15
21
|
const STATUS_STYLES = {
|
|
16
22
|
Todo: { bg: "bg-slate-100", text: "text-slate-600", icon: Circle },
|
|
17
23
|
InProgress: { bg: "bg-blue-100", text: "text-blue-700", icon: PlayCircle },
|
|
@@ -25,22 +31,147 @@ const PRIORITY_CONFIG = {
|
|
|
25
31
|
Medium: { icon: Minus, color: "text-yellow-500", label: "Medium" },
|
|
26
32
|
Low: { icon: ArrowDown, color: "text-slate-400", label: "Low" },
|
|
27
33
|
};
|
|
34
|
+
const HANDLE_CLASS = "bg-slate-300! border-slate-400! w-2! h-2!";
|
|
28
35
|
function TaskGraphNode({ data }) {
|
|
29
36
|
const status = data.status;
|
|
30
37
|
const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.Todo;
|
|
31
38
|
const StatusIcon = statusStyle.icon;
|
|
39
|
+
const executionDisplay = data.showExecutionStateBadge
|
|
40
|
+
? getTaskExecutionDisplayFromTask({
|
|
41
|
+
taskStatus: status,
|
|
42
|
+
executionState: data.executionState,
|
|
43
|
+
isBlocked: data.isBlocked,
|
|
44
|
+
})
|
|
45
|
+
: null;
|
|
32
46
|
const priorityCfg = data.priority ? PRIORITY_CONFIG[data.priority] : null;
|
|
33
47
|
const PriorityIcon = priorityCfg?.icon;
|
|
34
|
-
|
|
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
|
+
return (_jsxs("div", { className: cn("relative cursor-pointer rounded-lg border bg-white px-3 py-2 shadow-sm transition-all", data.isSelected
|
|
35
50
|
? "ring-primary border-primary/50 ring-2"
|
|
36
|
-
: "border-slate-200 hover:border-slate-300 hover:shadow-md", data.isBlocked && !data.isSelected && "border-red-200 bg-red-50/40"), style: { width: NODE_WIDTH },
|
|
51
|
+
: "border-slate-200 hover:border-slate-300 hover:shadow-md", data.isBlocked && !data.isSelected && "border-red-200 bg-red-50/40"), 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: "text-slate-400 hover:bg-red-50 hover:text-red-600 -mr-0.5 -mt-px shrink-0 rounded p-0.5 transition-colors", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
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 uppercase tracking-wider", 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, "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"), "aria-label": "Add dependent task", "data-testid": `dependency-graph-add-dependent-task-${data.taskId}`, onClick: (event) => {
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
data.onSelect(data.taskId);
|
|
57
|
+
data.onAddDependentTaskFromNode?.(data.taskId);
|
|
58
|
+
}, onMouseDown: (event) => {
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: "right", 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, "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]"), "aria-label": "Add subtask", "data-testid": `dependency-graph-add-subtask-${data.taskId}`, onClick: (event) => {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
data.onSelect(data.taskId);
|
|
63
|
+
data.onAddChildTaskFromNode?.(data.taskId);
|
|
64
|
+
}, onMouseDown: (event) => {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
}, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: "bottom", 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}` })] }));
|
|
37
67
|
}
|
|
38
68
|
const nodeTypes = {
|
|
39
69
|
taskNode: TaskGraphNode,
|
|
40
70
|
};
|
|
41
|
-
|
|
71
|
+
const HIERARCHY_CHILD_INDENT = 24;
|
|
72
|
+
const HIERARCHY_CHILD_GAP = 32;
|
|
73
|
+
const HIERARCHY_SUBTREE_GAP = 80;
|
|
74
|
+
/**
|
|
75
|
+
* Compound layout for the hierarchy strategy: each parent–children group is
|
|
76
|
+
* laid out vertically (parent on top, children stacked below with a slight
|
|
77
|
+
* indent), and then the resulting subtree "super-nodes" are arranged
|
|
78
|
+
* left-to-right via Dagre using the dependency edges between them.
|
|
79
|
+
*/
|
|
80
|
+
function layoutGraphHierarchy(nodes, edges) {
|
|
81
|
+
if (nodes.length === 0)
|
|
82
|
+
return { nodes, edges };
|
|
83
|
+
const childrenOf = new Map();
|
|
84
|
+
const parentOf = new Map();
|
|
85
|
+
for (const edge of edges) {
|
|
86
|
+
if (edge.data?.relationship !== "hierarchy")
|
|
87
|
+
continue;
|
|
88
|
+
parentOf.set(edge.target, edge.source);
|
|
89
|
+
const kids = childrenOf.get(edge.source) ?? [];
|
|
90
|
+
kids.push(edge.target);
|
|
91
|
+
childrenOf.set(edge.source, kids);
|
|
92
|
+
}
|
|
93
|
+
const roots = nodes.filter((n) => !parentOf.has(n.id));
|
|
94
|
+
function measureSubtree(nodeId, x, y, positions) {
|
|
95
|
+
positions.set(nodeId, { x, y });
|
|
96
|
+
const children = childrenOf.get(nodeId) ?? [];
|
|
97
|
+
if (children.length === 0) {
|
|
98
|
+
return { width: NODE_WIDTH, height: NODE_HEIGHT };
|
|
99
|
+
}
|
|
100
|
+
let childY = y + NODE_HEIGHT + HIERARCHY_CHILD_GAP;
|
|
101
|
+
let maxRight = x + NODE_WIDTH;
|
|
102
|
+
for (const childId of children) {
|
|
103
|
+
const result = measureSubtree(childId, x + HIERARCHY_CHILD_INDENT, childY, positions);
|
|
104
|
+
childY += result.height + HIERARCHY_CHILD_GAP;
|
|
105
|
+
maxRight = Math.max(maxRight, x + HIERARCHY_CHILD_INDENT + result.width);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
width: maxRight - x,
|
|
109
|
+
height: childY - HIERARCHY_CHILD_GAP - y,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const subtrees = new Map();
|
|
113
|
+
for (const root of roots) {
|
|
114
|
+
const positions = new Map();
|
|
115
|
+
const dims = measureSubtree(root.id, 0, 0, positions);
|
|
116
|
+
subtrees.set(root.id, { positions, ...dims });
|
|
117
|
+
}
|
|
118
|
+
const nodeToRoot = new Map();
|
|
119
|
+
for (const [rootId, data] of subtrees) {
|
|
120
|
+
for (const nodeId of data.positions.keys()) {
|
|
121
|
+
nodeToRoot.set(nodeId, rootId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
42
124
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
43
|
-
g.setGraph({ rankdir: "
|
|
125
|
+
g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: HIERARCHY_SUBTREE_GAP });
|
|
126
|
+
for (const [rootId, data] of subtrees) {
|
|
127
|
+
g.setNode(rootId, { width: data.width, height: data.height });
|
|
128
|
+
}
|
|
129
|
+
const addedSuperEdges = new Set();
|
|
130
|
+
for (const edge of edges) {
|
|
131
|
+
if (edge.data?.relationship !== "dependency")
|
|
132
|
+
continue;
|
|
133
|
+
const srcRoot = nodeToRoot.get(edge.source);
|
|
134
|
+
const tgtRoot = nodeToRoot.get(edge.target);
|
|
135
|
+
if (srcRoot && tgtRoot && srcRoot !== tgtRoot) {
|
|
136
|
+
const key = `${srcRoot}->${tgtRoot}`;
|
|
137
|
+
if (!addedSuperEdges.has(key)) {
|
|
138
|
+
addedSuperEdges.add(key);
|
|
139
|
+
g.setEdge(srcRoot, tgtRoot);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
Dagre.layout(g);
|
|
144
|
+
const finalPositions = new Map();
|
|
145
|
+
for (const [rootId, data] of subtrees) {
|
|
146
|
+
const superNode = g.node(rootId);
|
|
147
|
+
const offsetX = superNode.x - data.width / 2;
|
|
148
|
+
const offsetY = superNode.y - data.height / 2;
|
|
149
|
+
for (const [nodeId, localPos] of data.positions) {
|
|
150
|
+
finalPositions.set(nodeId, {
|
|
151
|
+
x: offsetX + localPos.x,
|
|
152
|
+
y: offsetY + localPos.y,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
nodes: nodes.map((node) => ({
|
|
158
|
+
...node,
|
|
159
|
+
position: finalPositions.get(node.id) ?? node.position,
|
|
160
|
+
})),
|
|
161
|
+
edges,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function layoutGraph(nodes, edges, orientation, autoLayoutStrategy) {
|
|
165
|
+
if (autoLayoutStrategy === "hierarchy" &&
|
|
166
|
+
edges.some((e) => e.data?.relationship === "hierarchy")) {
|
|
167
|
+
return layoutGraphHierarchy(nodes, edges);
|
|
168
|
+
}
|
|
169
|
+
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
170
|
+
g.setGraph({
|
|
171
|
+
rankdir: orientation === "horizontal" ? "LR" : "TB",
|
|
172
|
+
nodesep: 40,
|
|
173
|
+
ranksep: 60,
|
|
174
|
+
});
|
|
44
175
|
for (const node of nodes) {
|
|
45
176
|
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
46
177
|
}
|
|
@@ -60,11 +191,14 @@ function layoutGraph(nodes, edges) {
|
|
|
60
191
|
});
|
|
61
192
|
return { nodes: layoutedNodes, edges };
|
|
62
193
|
}
|
|
63
|
-
function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect) {
|
|
194
|
+
function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask) {
|
|
64
195
|
const taskMap = new Map();
|
|
65
196
|
for (const t of tasks)
|
|
66
197
|
taskMap.set(t.taskId, t);
|
|
67
198
|
const visibleTasks = tasks;
|
|
199
|
+
const childRelationshipKeys = new Set(visibleTasks
|
|
200
|
+
.filter((task) => !!task.parentTaskId)
|
|
201
|
+
.map((task) => `${task.parentTaskId}->${task.taskId}`));
|
|
68
202
|
const nodes = visibleTasks.map((task) => ({
|
|
69
203
|
id: task.taskId,
|
|
70
204
|
type: "taskNode",
|
|
@@ -74,41 +208,85 @@ function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelec
|
|
|
74
208
|
taskKey: task.taskKey ?? null,
|
|
75
209
|
title: task.title,
|
|
76
210
|
status: normalizeTaskStatus(task.status, task.executionState),
|
|
211
|
+
executionState: task.executionState,
|
|
212
|
+
showExecutionStateBadge: showExecutionStateBadges,
|
|
77
213
|
priority: task.priority ?? null,
|
|
78
214
|
assigneeDisplayName: task.assigneeDisplayName ?? null,
|
|
79
215
|
isSelected: task.taskId === selectedTaskId,
|
|
80
216
|
isBlocked: blockedTaskIds.has(task.taskId),
|
|
217
|
+
orientation,
|
|
81
218
|
onSelect,
|
|
219
|
+
onAddDependentTaskFromNode,
|
|
220
|
+
onAddChildTaskFromNode,
|
|
221
|
+
onRemoveTask,
|
|
82
222
|
},
|
|
83
223
|
}));
|
|
84
|
-
const
|
|
224
|
+
const depSourceHandle = orientation === "horizontal" ? "right" : "bottom";
|
|
225
|
+
const depTargetHandle = orientation === "horizontal" ? "left" : "top";
|
|
226
|
+
const hierSourceHandle = autoLayoutStrategy === "hierarchy" ? "bottom" : depSourceHandle;
|
|
227
|
+
const hierTargetHandle = autoLayoutStrategy === "hierarchy" ? "left" : depTargetHandle;
|
|
228
|
+
const dependencyEdges = dependencies
|
|
85
229
|
.filter((dep) => taskMap.has(dep.taskId) &&
|
|
86
|
-
taskMap.has(dep.dependsOnTaskId)
|
|
230
|
+
taskMap.has(dep.dependsOnTaskId) &&
|
|
231
|
+
!childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
|
|
87
232
|
.map((dep) => {
|
|
88
233
|
const isBlockedBy = dep.dependencyType === "BlockedBy";
|
|
234
|
+
const strokeColor = isBlockedBy
|
|
235
|
+
? EDGE_BLOCKED_BY
|
|
236
|
+
: EDGE_RELATED;
|
|
89
237
|
return {
|
|
90
238
|
id: dep.dependencyId,
|
|
91
239
|
source: dep.dependsOnTaskId,
|
|
92
240
|
target: dep.taskId,
|
|
93
|
-
|
|
241
|
+
sourceHandle: depSourceHandle,
|
|
242
|
+
targetHandle: depTargetHandle,
|
|
243
|
+
type: "smoothstep",
|
|
244
|
+
animated: false,
|
|
94
245
|
style: {
|
|
95
|
-
stroke:
|
|
246
|
+
stroke: strokeColor,
|
|
96
247
|
strokeWidth: isBlockedBy ? 2 : 1.5,
|
|
97
|
-
strokeDasharray: isBlockedBy ? undefined : "5 5",
|
|
98
248
|
},
|
|
99
249
|
markerEnd: {
|
|
100
250
|
type: MarkerType.ArrowClosed,
|
|
101
|
-
color:
|
|
251
|
+
color: strokeColor,
|
|
102
252
|
width: 16,
|
|
103
253
|
height: 16,
|
|
104
254
|
},
|
|
105
255
|
label: isBlockedBy ? undefined : "related",
|
|
106
|
-
labelStyle: { fontSize: 10, fill:
|
|
256
|
+
labelStyle: { fontSize: 10, fill: EDGE_RELATED },
|
|
257
|
+
data: {
|
|
258
|
+
relationship: "dependency",
|
|
259
|
+
},
|
|
107
260
|
};
|
|
108
261
|
});
|
|
262
|
+
const hierarchyEdges = visibleTasks
|
|
263
|
+
.filter((task) => !!task.parentTaskId &&
|
|
264
|
+
taskMap.has(task.parentTaskId))
|
|
265
|
+
.map((task) => ({
|
|
266
|
+
id: `parent:${task.parentTaskId}:${task.taskId}`,
|
|
267
|
+
source: task.parentTaskId,
|
|
268
|
+
target: task.taskId,
|
|
269
|
+
sourceHandle: hierSourceHandle,
|
|
270
|
+
targetHandle: hierTargetHandle,
|
|
271
|
+
type: "smoothstep",
|
|
272
|
+
style: {
|
|
273
|
+
stroke: "#3b82f6",
|
|
274
|
+
strokeWidth: 1.5,
|
|
275
|
+
},
|
|
276
|
+
markerEnd: {
|
|
277
|
+
type: MarkerType.ArrowClosed,
|
|
278
|
+
color: "#3b82f6",
|
|
279
|
+
width: 16,
|
|
280
|
+
height: 16,
|
|
281
|
+
},
|
|
282
|
+
data: {
|
|
283
|
+
relationship: "hierarchy",
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
const edges = [...dependencyEdges, ...hierarchyEdges];
|
|
109
287
|
if (nodes.length === 0)
|
|
110
288
|
return { nodes: [], edges: [] };
|
|
111
|
-
return layoutGraph(nodes, edges);
|
|
289
|
+
return layoutGraph(nodes, edges, orientation, autoLayoutStrategy);
|
|
112
290
|
}
|
|
113
291
|
function applySavedLayout(nodes, savedLayout) {
|
|
114
292
|
if (!savedLayout?.nodes?.length) {
|
|
@@ -133,9 +311,65 @@ function preserveNodePositions(previousNodes, nextNodes) {
|
|
|
133
311
|
position: previousPositions.get(node.id) ?? node.position,
|
|
134
312
|
}));
|
|
135
313
|
}
|
|
136
|
-
function
|
|
314
|
+
function buildChildRelationshipKeys(tasks) {
|
|
315
|
+
return new Set(tasks
|
|
316
|
+
.filter((task) => !!task.parentTaskId)
|
|
317
|
+
.map((task) => `${task.parentTaskId}->${task.taskId}`));
|
|
318
|
+
}
|
|
319
|
+
function isHierarchyConnectionValid(connection, tasks) {
|
|
320
|
+
const { source, sourceHandle, target, targetHandle } = connection;
|
|
321
|
+
if (!source || !target || source === target) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (sourceHandle !== "bottom") {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const validTaskIds = new Set(tasks.map((task) => task.taskId));
|
|
331
|
+
if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const parentByTaskId = new Map(tasks.map((task) => [task.taskId, task.parentTaskId ?? null]));
|
|
335
|
+
let ancestorTaskId = parentByTaskId.get(source) ?? null;
|
|
336
|
+
while (ancestorTaskId) {
|
|
337
|
+
if (ancestorTaskId === target) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
ancestorTaskId = parentByTaskId.get(ancestorTaskId) ?? null;
|
|
341
|
+
}
|
|
342
|
+
return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
|
|
343
|
+
}
|
|
344
|
+
function isDependencyConnectionValid(connection, tasks, dependencies) {
|
|
345
|
+
const { source, sourceHandle, target, targetHandle } = connection;
|
|
346
|
+
if (!source || !target || source === target) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (sourceHandle !== "right") {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const validTaskIds = new Set(tasks.map((task) => task.taskId));
|
|
356
|
+
if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (dependencies.some((dependency) => dependency.dependsOnTaskId === source && dependency.taskId === target)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
|
|
363
|
+
}
|
|
364
|
+
function isGraphConnectionValid(connection, tasks, dependencies) {
|
|
365
|
+
if (connection.sourceHandle === "bottom") {
|
|
366
|
+
return isHierarchyConnectionValid(connection, tasks);
|
|
367
|
+
}
|
|
368
|
+
return isDependencyConnectionValid(connection, tasks, dependencies);
|
|
369
|
+
}
|
|
370
|
+
function createLayoutSignature(layoutKey, nodes, viewport) {
|
|
137
371
|
return JSON.stringify({
|
|
138
|
-
|
|
372
|
+
layoutKey,
|
|
139
373
|
nodes: nodes
|
|
140
374
|
.map((node) => ({
|
|
141
375
|
taskId: node.id,
|
|
@@ -153,8 +387,11 @@ function createLayoutSignature(projectId, nodes, viewport) {
|
|
|
153
387
|
});
|
|
154
388
|
}
|
|
155
389
|
export function DependencyGraphView(props) {
|
|
156
|
-
const { projectId, tasks, dependencies, savedLayout, selectedTaskId, onSelectTask, permission, onLayoutSaved, } = props;
|
|
157
|
-
const canPersistLayout =
|
|
390
|
+
const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, onSelectTask, 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;
|
|
391
|
+
const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
|
|
392
|
+
? canPersistLayoutOverride
|
|
393
|
+
: permission === "Owner" || permission === "Editor";
|
|
394
|
+
const effectiveLayoutKey = layoutKey ?? projectId ?? "graph";
|
|
158
395
|
const taskMap = useMemo(() => {
|
|
159
396
|
const m = new Map();
|
|
160
397
|
for (const t of tasks)
|
|
@@ -162,6 +399,9 @@ export function DependencyGraphView(props) {
|
|
|
162
399
|
return m;
|
|
163
400
|
}, [tasks]);
|
|
164
401
|
const blockedTaskIds = useMemo(() => {
|
|
402
|
+
if (!highlightBlockedTasks) {
|
|
403
|
+
return new Set();
|
|
404
|
+
}
|
|
165
405
|
const blocked = new Set();
|
|
166
406
|
for (const dep of dependencies) {
|
|
167
407
|
if (dep.dependencyType !== "BlockedBy")
|
|
@@ -173,9 +413,21 @@ export function DependencyGraphView(props) {
|
|
|
173
413
|
}
|
|
174
414
|
}
|
|
175
415
|
return blocked;
|
|
176
|
-
}, [dependencies, taskMap]);
|
|
416
|
+
}, [dependencies, highlightBlockedTasks, taskMap]);
|
|
177
417
|
const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
|
|
178
|
-
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds,
|
|
418
|
+
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
|
|
419
|
+
tasks,
|
|
420
|
+
dependencies,
|
|
421
|
+
selectedTaskId,
|
|
422
|
+
blockedTaskIds,
|
|
423
|
+
orientation,
|
|
424
|
+
showExecutionStateBadges,
|
|
425
|
+
autoLayoutStrategy,
|
|
426
|
+
onSelect,
|
|
427
|
+
onAddDependentTaskFromNode,
|
|
428
|
+
onAddChildTaskFromNode,
|
|
429
|
+
onRemoveTask,
|
|
430
|
+
]);
|
|
179
431
|
const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
|
|
180
432
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
181
433
|
const nodesRef = useRef(nodes);
|
|
@@ -198,7 +450,7 @@ export function DependencyGraphView(props) {
|
|
|
198
450
|
const skipNextMoveEndSaveRef = useRef(false);
|
|
199
451
|
const saveAfterViewportRestoreRef = useRef(false);
|
|
200
452
|
const lastSavedSignatureRef = useRef(null);
|
|
201
|
-
const
|
|
453
|
+
const lastAppliedLayoutKeyRef = useRef(null);
|
|
202
454
|
const lastAppliedLayoutSignatureRef = useRef(null);
|
|
203
455
|
const savedLayoutSignature = useMemo(() => savedLayout
|
|
204
456
|
? JSON.stringify({
|
|
@@ -211,56 +463,85 @@ export function DependencyGraphView(props) {
|
|
|
211
463
|
if (!canPersistLayout) {
|
|
212
464
|
return;
|
|
213
465
|
}
|
|
214
|
-
const signature = createLayoutSignature(
|
|
466
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
|
|
215
467
|
if (signature === lastSavedSignatureRef.current) {
|
|
216
468
|
return;
|
|
217
469
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
470
|
+
try {
|
|
471
|
+
const layoutPayload = {
|
|
472
|
+
nodes: nextNodes.map((node) => ({
|
|
473
|
+
taskId: node.id,
|
|
474
|
+
x: node.position.x,
|
|
475
|
+
y: node.position.y,
|
|
476
|
+
})),
|
|
477
|
+
viewport: viewport == null
|
|
478
|
+
? null
|
|
479
|
+
: {
|
|
480
|
+
x: viewport.x,
|
|
481
|
+
y: viewport.y,
|
|
482
|
+
zoom: viewport.zoom,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
const nextLayout = onPersistLayout
|
|
486
|
+
? await onPersistLayout(layoutPayload)
|
|
487
|
+
: projectId
|
|
488
|
+
? await (async () => {
|
|
489
|
+
const result = await saveGraphLayout({
|
|
490
|
+
projectId,
|
|
491
|
+
nodes: layoutPayload.nodes,
|
|
492
|
+
viewport: layoutPayload.viewport,
|
|
493
|
+
});
|
|
494
|
+
if (result.type !== "success") {
|
|
495
|
+
toast.error(result.summary || "Failed to save graph layout");
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
return result.data ?? null;
|
|
499
|
+
})()
|
|
500
|
+
: layoutPayload;
|
|
501
|
+
lastSavedSignatureRef.current = signature;
|
|
502
|
+
if (nextLayout) {
|
|
503
|
+
const incomingSignature = JSON.stringify({
|
|
504
|
+
updatedAt: nextLayout.updatedAt ?? null,
|
|
505
|
+
nodes: nextLayout.nodes ?? [],
|
|
506
|
+
viewport: nextLayout.viewport ?? null,
|
|
507
|
+
});
|
|
508
|
+
lastAppliedLayoutSignatureRef.current = incomingSignature;
|
|
509
|
+
}
|
|
510
|
+
onLayoutSaved?.(nextLayout ?? null);
|
|
236
511
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
nodes: result.data.nodes ?? [],
|
|
242
|
-
viewport: result.data.viewport ?? null,
|
|
243
|
-
});
|
|
244
|
-
lastAppliedLayoutSignatureRef.current = incomingSignature;
|
|
512
|
+
catch (error) {
|
|
513
|
+
toast.error(error instanceof Error
|
|
514
|
+
? error.message
|
|
515
|
+
: "Failed to save graph layout");
|
|
245
516
|
}
|
|
246
|
-
|
|
247
|
-
|
|
517
|
+
}, [
|
|
518
|
+
canPersistLayout,
|
|
519
|
+
effectiveLayoutKey,
|
|
520
|
+
onLayoutSaved,
|
|
521
|
+
onPersistLayout,
|
|
522
|
+
projectId,
|
|
523
|
+
]);
|
|
248
524
|
const queueLayoutSave = useCallback((nextNodes, viewport) => {
|
|
249
525
|
if (!canPersistLayout) {
|
|
250
526
|
return;
|
|
251
527
|
}
|
|
252
|
-
const signature = createLayoutSignature(
|
|
528
|
+
const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
|
|
253
529
|
if (signature === lastSavedSignatureRef.current) {
|
|
254
530
|
return;
|
|
255
531
|
}
|
|
256
532
|
if (saveTimeoutRef.current !== null) {
|
|
257
533
|
window.clearTimeout(saveTimeoutRef.current);
|
|
534
|
+
saveTimeoutRef.current = null;
|
|
535
|
+
}
|
|
536
|
+
if (layoutSaveDebounceMs === 0) {
|
|
537
|
+
void persistLayout(nextNodes, viewport);
|
|
538
|
+
return;
|
|
258
539
|
}
|
|
259
540
|
saveTimeoutRef.current = window.setTimeout(() => {
|
|
260
541
|
saveTimeoutRef.current = null;
|
|
261
542
|
void persistLayout(nextNodes, viewport);
|
|
262
|
-
},
|
|
263
|
-
}, [canPersistLayout,
|
|
543
|
+
}, layoutSaveDebounceMs);
|
|
544
|
+
}, [canPersistLayout, effectiveLayoutKey, layoutSaveDebounceMs, persistLayout]);
|
|
264
545
|
const restoreViewport = useCallback(() => {
|
|
265
546
|
const instance = reactFlowRef.current;
|
|
266
547
|
if (!instance || nodesRef.current.length === 0) {
|
|
@@ -272,7 +553,7 @@ export function DependencyGraphView(props) {
|
|
|
272
553
|
skipNextMoveEndSaveRef.current = true;
|
|
273
554
|
window.requestAnimationFrame(() => {
|
|
274
555
|
reactFlowRef.current?.setViewport(viewport, { duration: 0 });
|
|
275
|
-
lastSavedSignatureRef.current = createLayoutSignature(
|
|
556
|
+
lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport);
|
|
276
557
|
});
|
|
277
558
|
return;
|
|
278
559
|
}
|
|
@@ -290,9 +571,9 @@ export function DependencyGraphView(props) {
|
|
|
290
571
|
const viewport = reactFlowRef.current?.getViewport() ?? null;
|
|
291
572
|
queueLayoutSave(nodesRef.current, viewport);
|
|
292
573
|
});
|
|
293
|
-
}, [
|
|
574
|
+
}, [effectiveLayoutKey, queueLayoutSave]);
|
|
294
575
|
useEffect(() => {
|
|
295
|
-
const shouldApplySavedLayout =
|
|
576
|
+
const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
|
|
296
577
|
lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
|
|
297
578
|
const nextNodes = shouldApplySavedLayout
|
|
298
579
|
? applySavedLayout(initialNodes, savedLayout)
|
|
@@ -301,13 +582,13 @@ export function DependencyGraphView(props) {
|
|
|
301
582
|
setEdges(initialEdges);
|
|
302
583
|
nodesRef.current = nextNodes;
|
|
303
584
|
if (shouldApplySavedLayout) {
|
|
304
|
-
|
|
585
|
+
lastAppliedLayoutKeyRef.current = effectiveLayoutKey;
|
|
305
586
|
lastAppliedLayoutSignatureRef.current = savedLayoutSignature;
|
|
306
587
|
pendingViewportRef.current = savedLayout?.viewport ?? null;
|
|
307
588
|
fitViewOnNextSyncRef.current = !savedLayout?.viewport;
|
|
308
589
|
if (!savedLayout?.viewport) {
|
|
309
590
|
lastSavedSignatureRef.current = savedLayout?.nodes?.length
|
|
310
|
-
? createLayoutSignature(
|
|
591
|
+
? createLayoutSignature(effectiveLayoutKey, nextNodes, null)
|
|
311
592
|
: null;
|
|
312
593
|
}
|
|
313
594
|
restoreViewport();
|
|
@@ -315,7 +596,7 @@ export function DependencyGraphView(props) {
|
|
|
315
596
|
}, [
|
|
316
597
|
initialEdges,
|
|
317
598
|
initialNodes,
|
|
318
|
-
|
|
599
|
+
effectiveLayoutKey,
|
|
319
600
|
restoreViewport,
|
|
320
601
|
savedLayout,
|
|
321
602
|
savedLayoutSignature,
|
|
@@ -332,7 +613,7 @@ export function DependencyGraphView(props) {
|
|
|
332
613
|
}, []);
|
|
333
614
|
const hasVisibleNodes = initialNodes.length > 0;
|
|
334
615
|
const handleAutoLayout = useCallback(() => {
|
|
335
|
-
const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect);
|
|
616
|
+
const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
|
|
336
617
|
setNodes(autoLayout.nodes);
|
|
337
618
|
setEdges(autoLayout.edges);
|
|
338
619
|
nodesRef.current = autoLayout.nodes;
|
|
@@ -343,15 +624,51 @@ export function DependencyGraphView(props) {
|
|
|
343
624
|
blockedTaskIds,
|
|
344
625
|
dependencies,
|
|
345
626
|
onSelect,
|
|
627
|
+
orientation,
|
|
628
|
+
showExecutionStateBadges,
|
|
629
|
+
autoLayoutStrategy,
|
|
630
|
+
onRemoveTask,
|
|
631
|
+
onAddDependentTaskFromNode,
|
|
632
|
+
onAddChildTaskFromNode,
|
|
346
633
|
restoreViewport,
|
|
347
634
|
selectedTaskId,
|
|
348
635
|
setEdges,
|
|
349
636
|
setNodes,
|
|
350
637
|
tasks,
|
|
351
638
|
]);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
639
|
+
const isValidConnection = useCallback((connection) => allowDependencyConnect
|
|
640
|
+
? isGraphConnectionValid(connection, tasks, dependencies)
|
|
641
|
+
: false, [allowDependencyConnect, dependencies, tasks]);
|
|
642
|
+
const handleConnect = useCallback((connection) => {
|
|
643
|
+
if (!allowDependencyConnect) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const { source, sourceHandle, target } = connection;
|
|
647
|
+
if (!source || !target) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (!isGraphConnectionValid(connection, tasks, dependencies)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
onSelectTask(target);
|
|
654
|
+
if (sourceHandle === "bottom") {
|
|
655
|
+
onCreateChildRelationship?.(source, target);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
onCreateDependency?.(source, target);
|
|
659
|
+
}, [
|
|
660
|
+
allowDependencyConnect,
|
|
661
|
+
dependencies,
|
|
662
|
+
onCreateChildRelationship,
|
|
663
|
+
onCreateDependency,
|
|
664
|
+
onSelectTask,
|
|
665
|
+
tasks,
|
|
666
|
+
]);
|
|
667
|
+
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-2 w-4 rounded-sm border border-orange-300 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 && 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, onNodeClick: (_event, node) => {
|
|
668
|
+
onSelectTask(node.id);
|
|
669
|
+
}, onNodeDragStart: (_event, node) => {
|
|
670
|
+
onSelectTask(node.id);
|
|
671
|
+
}, onInit: (instance) => {
|
|
355
672
|
reactFlowRef.current = instance;
|
|
356
673
|
restoreViewport();
|
|
357
674
|
}, onNodeDragStop: () => {
|
|
@@ -364,7 +681,9 @@ export function DependencyGraphView(props) {
|
|
|
364
681
|
return;
|
|
365
682
|
}
|
|
366
683
|
queueLayoutSave(nodesRef.current, viewport);
|
|
367
|
-
}, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable:
|
|
684
|
+
}, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: false, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), _jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
|
|
685
|
+
? { width: miniMapWidth, height: miniMapHeight }
|
|
686
|
+
: {}), nodeColor: (node) => {
|
|
368
687
|
const status = node.data?.status;
|
|
369
688
|
switch (status) {
|
|
370
689
|
case "Done":
|