@parhelia/core 0.1.12368 → 0.1.12393
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 +37 -15
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/SetupWizard.js +20 -2
- package/dist/editor/SetupWizard.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 +158 -39
- 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-editor-chrome/useInlineAICompletion.js +283 -298
- package/dist/editor/page-editor-chrome/useInlineAICompletion.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/services/templateBuilderService.d.ts +7 -0
- package/dist/editor/services/templateBuilderService.js +7 -1
- package/dist/editor/services/templateBuilderService.js.map +1 -1
- package/dist/editor/settings/About.js +25 -19
- package/dist/editor/settings/About.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/setup-wizard/steps/CompleteStep.d.ts +2 -1
- package/dist/editor/setup-wizard/steps/CompleteStep.js +2 -1
- package/dist/editor/setup-wizard/steps/CompleteStep.js.map +1 -1
- package/dist/editor/setup-wizard/steps/LicenseActivationStep.d.ts +9 -0
- package/dist/editor/setup-wizard/steps/LicenseActivationStep.js +160 -0
- package/dist/editor/setup-wizard/steps/LicenseActivationStep.js.map +1 -0
- package/dist/editor/setup-wizard/steps/LicenseEmailStep.d.ts +10 -0
- package/dist/editor/setup-wizard/steps/LicenseEmailStep.js +101 -0
- package/dist/editor/setup-wizard/steps/LicenseEmailStep.js.map +1 -0
- package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +422 -65
- package/dist/editor/template-wizard/TemplateStructureInlineEditor.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/licensing/EmailEntry.js +1 -1
- package/dist/licensing/EmailEntry.js.map +1 -1
- package/dist/licensing/LicenseActivationForm.js +1 -1
- package/dist/licensing/LicenseActivationForm.js.map +1 -1
- package/dist/licensing/LicenseCodeEntry.js +2 -2
- package/dist/licensing/LicenseCodeEntry.js.map +1 -1
- package/dist/licensing/LicenseContext.js +18 -9
- package/dist/licensing/LicenseContext.js.map +1 -1
- package/dist/licensing/LicenseOverlay.js +2 -1
- package/dist/licensing/LicenseOverlay.js.map +1 -1
- package/dist/licensing/licenseService.d.ts +10 -0
- package/dist/licensing/licenseService.js +28 -0
- package/dist/licensing/licenseService.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
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { AlertCircle, ChevronDown, CornerDownRight, GitBranch, Link2, Plus, RefreshCw, Search, Settings2, Shield, Trash2, X, } from "lucide-react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Button } from "../../../components/ui/button";
|
|
6
|
+
import { Dialog, DialogContent, DialogFooter, } from "../../../components/ui/dialog";
|
|
7
|
+
import { Input } from "../../../components/ui/input";
|
|
8
|
+
import { Label } from "../../../components/ui/label";
|
|
9
|
+
import { StyledDialogTitle } from "../../../components/ui/styled-dialog-title";
|
|
10
|
+
import { Textarea } from "../../../components/ui/textarea";
|
|
11
|
+
import { Select } from "../../../components/ui/select";
|
|
12
|
+
import { Switch } from "../../../components/ui/switch";
|
|
13
|
+
import { Badge } from "../../../components/ui/badge";
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger, } from "../../../components/ui/popover";
|
|
15
|
+
import { Splitter } from "../../ui/Splitter";
|
|
16
|
+
import { DependencyGraphView } from "../../../task-board/views/DependencyGraphView";
|
|
17
|
+
import { deleteProjectTemplate, getProjectTemplates, getProjectTemplateAgent, resetProjectTemplateAgent, upsertProjectTemplate, } from "../../../task-board/services/taskService";
|
|
18
|
+
import { getAiProfilesErrorMessage, loadAiProfiles, } from "../../services/aiService";
|
|
19
|
+
import { updateAgentContext } from "../../services/agentService";
|
|
20
|
+
import { useEditContext } from "../../client/editContext";
|
|
21
|
+
import { isTypingEventTarget } from "../../utils/keyboardNavigation";
|
|
22
|
+
import { cn } from "../../../lib/utils";
|
|
23
|
+
import { ProjectTemplateAgentPanel } from "./ProjectTemplateAgentPanel";
|
|
24
|
+
import { useSearchParams } from "next/navigation";
|
|
25
|
+
/** Query param for deep-linking the selected project template (settings reload). */
|
|
26
|
+
const SELECTED_PROJECT_TEMPLATE_QUERY_PARAM = "projectTemplateId";
|
|
27
|
+
const PRIORITY_OPTIONS = [
|
|
28
|
+
{ value: "", label: "No priority" },
|
|
29
|
+
{ value: "Low", label: "Low" },
|
|
30
|
+
{ value: "Medium", label: "Medium" },
|
|
31
|
+
{ value: "High", label: "High" },
|
|
32
|
+
{ value: "Critical", label: "Critical" },
|
|
33
|
+
];
|
|
34
|
+
const ASSIGNEE_TYPE_OPTIONS = [
|
|
35
|
+
{ value: "", label: "Unassigned" },
|
|
36
|
+
{ value: "Agent", label: "Agent profile" },
|
|
37
|
+
{ value: "Human", label: "Human user" },
|
|
38
|
+
{ value: "Role", label: "Role" },
|
|
39
|
+
];
|
|
40
|
+
const PROJECT_TEMPLATE_AUTOSAVE_DEBOUNCE_MS = 1200;
|
|
41
|
+
function cloneTemplate(template) {
|
|
42
|
+
return normalizeProjectTemplate(JSON.parse(JSON.stringify(template)));
|
|
43
|
+
}
|
|
44
|
+
function normalizeTaskTemplate(task) {
|
|
45
|
+
const rawAssigneeId = task.assigneeId?.trim() || null;
|
|
46
|
+
const rawAgentProfileId = task.agentProfileId?.trim() || null;
|
|
47
|
+
const rawParentTaskId = task.parentTaskId?.trim() || null;
|
|
48
|
+
const assigneeType = task.assigneeType ?? (rawAgentProfileId ? "Agent" : null);
|
|
49
|
+
if (assigneeType === "Agent") {
|
|
50
|
+
const canonicalAgentId = rawAgentProfileId || (isValidGuid(rawAssigneeId) ? rawAssigneeId : null);
|
|
51
|
+
return {
|
|
52
|
+
...task,
|
|
53
|
+
assigneeType: "Agent",
|
|
54
|
+
assigneeId: canonicalAgentId,
|
|
55
|
+
agentProfileId: canonicalAgentId,
|
|
56
|
+
parentTaskId: rawParentTaskId,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (assigneeType === "Human" || assigneeType === "Role") {
|
|
60
|
+
return {
|
|
61
|
+
...task,
|
|
62
|
+
assigneeType,
|
|
63
|
+
assigneeId: rawAssigneeId,
|
|
64
|
+
agentProfileId: null,
|
|
65
|
+
parentTaskId: rawParentTaskId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...task,
|
|
70
|
+
assigneeType: null,
|
|
71
|
+
assigneeId: rawAssigneeId,
|
|
72
|
+
agentProfileId: null,
|
|
73
|
+
parentTaskId: rawParentTaskId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeProjectTemplate(template) {
|
|
77
|
+
return {
|
|
78
|
+
...template,
|
|
79
|
+
hideFromCreateDialog: template.hideFromCreateDialog === true,
|
|
80
|
+
openInWizardMode: template.openInWizardMode === true,
|
|
81
|
+
taskTemplates: (template.taskTemplates ?? []).map(normalizeTaskTemplate),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createEmptyTemplate() {
|
|
85
|
+
return {
|
|
86
|
+
id: crypto.randomUUID(),
|
|
87
|
+
name: "New Project Template",
|
|
88
|
+
description: "",
|
|
89
|
+
disabled: false,
|
|
90
|
+
hideFromCreateDialog: false,
|
|
91
|
+
openInWizardMode: false,
|
|
92
|
+
isSystem: false,
|
|
93
|
+
defaultCostLimit: null,
|
|
94
|
+
graphLayout: null,
|
|
95
|
+
taskTemplates: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createEmptyTaskTemplate(taskCount, title) {
|
|
99
|
+
return {
|
|
100
|
+
id: crypto.randomUUID(),
|
|
101
|
+
title: title?.trim() || `Task ${taskCount + 1}`,
|
|
102
|
+
description: "",
|
|
103
|
+
agentProfileId: null,
|
|
104
|
+
assigneeType: null,
|
|
105
|
+
assigneeId: null,
|
|
106
|
+
priority: null,
|
|
107
|
+
sortOrder: taskCount * 100,
|
|
108
|
+
parentTaskId: null,
|
|
109
|
+
dependencyIds: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function sortTemplates(templates) {
|
|
113
|
+
return [...templates].sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
114
|
+
}
|
|
115
|
+
function buildUpsertTemplateRequest(template) {
|
|
116
|
+
return {
|
|
117
|
+
projectTemplateId: template.id,
|
|
118
|
+
name: template.name.trim(),
|
|
119
|
+
description: template.description?.trim() || null,
|
|
120
|
+
disabled: template.disabled === true,
|
|
121
|
+
hideFromCreateDialog: template.hideFromCreateDialog === true,
|
|
122
|
+
openInWizardMode: template.openInWizardMode === true,
|
|
123
|
+
defaultCostLimit: template.defaultCostLimit == null ||
|
|
124
|
+
Number.isNaN(template.defaultCostLimit)
|
|
125
|
+
? null
|
|
126
|
+
: template.defaultCostLimit,
|
|
127
|
+
graphLayout: template.graphLayout
|
|
128
|
+
? {
|
|
129
|
+
projectTemplateId: template.id,
|
|
130
|
+
nodes: template.graphLayout.nodes ?? [],
|
|
131
|
+
viewport: template.graphLayout.viewport ?? null,
|
|
132
|
+
}
|
|
133
|
+
: null,
|
|
134
|
+
taskTemplates: template.taskTemplates.map((task, index) => {
|
|
135
|
+
const normalizedTask = normalizeTaskTemplate(task);
|
|
136
|
+
return {
|
|
137
|
+
...normalizedTask,
|
|
138
|
+
title: normalizedTask.title.trim(),
|
|
139
|
+
description: normalizedTask.description?.trim() || null,
|
|
140
|
+
priority: normalizedTask.priority || null,
|
|
141
|
+
sortOrder: normalizedTask.sortOrder || index * 100,
|
|
142
|
+
parentTaskId: normalizedTask.parentTaskId || null,
|
|
143
|
+
agentProfileId: normalizedTask.agentProfileId || null,
|
|
144
|
+
assigneeType: normalizedTask.assigneeType || null,
|
|
145
|
+
assigneeId: normalizedTask.assigneeId || null,
|
|
146
|
+
dependencyIds: Array.from(new Set(normalizedTask.dependencyIds || [])),
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function mergeTemplateResponseWithRequest(serverTemplate, request) {
|
|
152
|
+
const parentTaskIdByTaskId = new Map((request.taskTemplates ?? []).map((task) => [
|
|
153
|
+
task.id,
|
|
154
|
+
task.parentTaskId ?? null,
|
|
155
|
+
]));
|
|
156
|
+
const mergedGraphLayout = request.graphLayout === undefined
|
|
157
|
+
? serverTemplate.graphLayout
|
|
158
|
+
: request.graphLayout === null
|
|
159
|
+
? null
|
|
160
|
+
: {
|
|
161
|
+
projectTemplateId: serverTemplate.id,
|
|
162
|
+
nodes: request.graphLayout.nodes ?? [],
|
|
163
|
+
viewport: request.graphLayout.viewport ?? null,
|
|
164
|
+
};
|
|
165
|
+
return normalizeProjectTemplate({
|
|
166
|
+
...serverTemplate,
|
|
167
|
+
graphLayout: mergedGraphLayout,
|
|
168
|
+
taskTemplates: (serverTemplate.taskTemplates ?? []).map((task) => ({
|
|
169
|
+
...task,
|
|
170
|
+
parentTaskId: task.parentTaskId ?? parentTaskIdByTaskId.get(task.id) ?? null,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function isValidGuid(value) {
|
|
175
|
+
return (!!value &&
|
|
176
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value));
|
|
177
|
+
}
|
|
178
|
+
function readProjectTemplateIdFromUrl(searchParams) {
|
|
179
|
+
const raw = searchParams.get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM) ||
|
|
180
|
+
(typeof window !== "undefined"
|
|
181
|
+
? new URLSearchParams(window.location.search).get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM)
|
|
182
|
+
: null);
|
|
183
|
+
return raw && isValidGuid(raw) ? raw : null;
|
|
184
|
+
}
|
|
185
|
+
function readProjectTemplateIdFromWindow() {
|
|
186
|
+
const raw = typeof window !== "undefined"
|
|
187
|
+
? new URLSearchParams(window.location.search).get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM)
|
|
188
|
+
: null;
|
|
189
|
+
return raw && isValidGuid(raw) ? raw : null;
|
|
190
|
+
}
|
|
191
|
+
function getSelectedTaskIdForTemplate(template, preferredTaskId) {
|
|
192
|
+
const taskTemplates = template?.taskTemplates ?? [];
|
|
193
|
+
if (!taskTemplates.length) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (preferredTaskId &&
|
|
197
|
+
taskTemplates.some((task) => task.id === preferredTaskId)) {
|
|
198
|
+
return preferredTaskId;
|
|
199
|
+
}
|
|
200
|
+
return taskTemplates[0]?.id ?? null;
|
|
201
|
+
}
|
|
202
|
+
function normalizeTaskPriority(priority) {
|
|
203
|
+
if (priority === "Low" ||
|
|
204
|
+
priority === "Medium" ||
|
|
205
|
+
priority === "High" ||
|
|
206
|
+
priority === "Critical") {
|
|
207
|
+
return priority;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function buildTemplateTaskItems(template, aiProfilesById) {
|
|
212
|
+
if (!template)
|
|
213
|
+
return [];
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
return template.taskTemplates.map((task) => {
|
|
216
|
+
const normalizedTask = normalizeTaskTemplate(task);
|
|
217
|
+
const assigneeType = normalizedTask.assigneeType;
|
|
218
|
+
const assigneeId = normalizedTask.assigneeId;
|
|
219
|
+
const assigneeDisplayName = assigneeType === "Agent" && assigneeId
|
|
220
|
+
? aiProfilesById[assigneeId.toLowerCase()]?.displayTitle ||
|
|
221
|
+
aiProfilesById[assigneeId.toLowerCase()]?.name ||
|
|
222
|
+
assigneeId
|
|
223
|
+
: assigneeId;
|
|
224
|
+
return {
|
|
225
|
+
taskId: task.id,
|
|
226
|
+
projectId: template.id,
|
|
227
|
+
parentTaskId: normalizedTask.parentTaskId ?? null,
|
|
228
|
+
taskType: "Task",
|
|
229
|
+
title: task.title || "Untitled Task",
|
|
230
|
+
description: normalizedTask.description ?? null,
|
|
231
|
+
status: "Todo",
|
|
232
|
+
assigneeType,
|
|
233
|
+
assigneeId,
|
|
234
|
+
assigneeDisplayName,
|
|
235
|
+
priority: normalizeTaskPriority(normalizedTask.priority),
|
|
236
|
+
sortOrder: normalizedTask.sortOrder,
|
|
237
|
+
executionState: "Unassigned",
|
|
238
|
+
assignedAgentId: null,
|
|
239
|
+
createdBy: "template",
|
|
240
|
+
createdAt: now,
|
|
241
|
+
updatedAt: now,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function buildTemplateDependencies(template) {
|
|
246
|
+
if (!template)
|
|
247
|
+
return [];
|
|
248
|
+
return template.taskTemplates.flatMap((task) => (task.dependencyIds ?? []).map((dependencyId) => ({
|
|
249
|
+
dependencyId: `${task.id}:${dependencyId}`,
|
|
250
|
+
taskId: task.id,
|
|
251
|
+
dependsOnTaskId: dependencyId,
|
|
252
|
+
dependencyType: "BlockedBy",
|
|
253
|
+
})));
|
|
254
|
+
}
|
|
255
|
+
function canAddTemplateDependency(template, taskId, dependencyId) {
|
|
256
|
+
if (!template || taskId === dependencyId) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const tasksById = new Map(template.taskTemplates.map((task) => [task.id, task]));
|
|
260
|
+
const task = tasksById.get(taskId);
|
|
261
|
+
const dependencyTask = tasksById.get(dependencyId);
|
|
262
|
+
if (!task || !dependencyTask) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if ((task.dependencyIds ?? []).includes(dependencyId)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return task.parentTaskId !== dependencyId;
|
|
269
|
+
}
|
|
270
|
+
function buildProjectTemplateAgentContext(template, selectedTaskTemplateId) {
|
|
271
|
+
const dependencyCount = template.taskTemplates.reduce((count, task) => count + (task.dependencyIds?.length ?? 0), 0);
|
|
272
|
+
const agentAssignedTaskCount = template.taskTemplates.filter((task) => task.agentProfileId || (task.assigneeType === "Agent" && task.assigneeId)).length;
|
|
273
|
+
const selectedTask = selectedTaskTemplateId
|
|
274
|
+
? template.taskTemplates.find((task) => task.id === selectedTaskTemplateId) ??
|
|
275
|
+
null
|
|
276
|
+
: null;
|
|
277
|
+
return {
|
|
278
|
+
activeWorkspace: "settings",
|
|
279
|
+
additionalData: {
|
|
280
|
+
activeWorkspace: "settings",
|
|
281
|
+
projectTemplateEditor: true,
|
|
282
|
+
projectTemplateId: template.id,
|
|
283
|
+
projectTemplateName: template.name,
|
|
284
|
+
projectTemplateDescription: template.description ?? null,
|
|
285
|
+
projectTemplateDisabled: template.disabled === true,
|
|
286
|
+
projectTemplateDefaultCostLimit: template.defaultCostLimit ?? null,
|
|
287
|
+
selectedTaskTemplateId: selectedTask ? selectedTask.id : null,
|
|
288
|
+
selectedTaskTemplateTitle: selectedTask?.title?.trim() || null,
|
|
289
|
+
selectedTaskTemplate: selectedTask
|
|
290
|
+
? normalizeTaskTemplate(selectedTask)
|
|
291
|
+
: null,
|
|
292
|
+
projectTemplateSummary: {
|
|
293
|
+
taskCount: template.taskTemplates.length,
|
|
294
|
+
dependencyCount,
|
|
295
|
+
agentAssignedTaskCount,
|
|
296
|
+
defaultCostLimit: template.defaultCostLimit ?? null,
|
|
297
|
+
disabled: template.disabled === true,
|
|
298
|
+
},
|
|
299
|
+
projectTemplate: cloneTemplate(template),
|
|
300
|
+
projectTemplateSavePayload: buildUpsertTemplateRequest(template),
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
export function ProjectTemplatesPanel() {
|
|
305
|
+
const editContext = useEditContext();
|
|
306
|
+
const searchParams = useSearchParams();
|
|
307
|
+
const [state, setState] = useState("loading");
|
|
308
|
+
const [templates, setTemplates] = useState([]);
|
|
309
|
+
const [error, setError] = useState(null);
|
|
310
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
311
|
+
const [selectedTemplateId, setSelectedTemplateId] = useState(null);
|
|
312
|
+
const [draftTemplate, setDraftTemplate] = useState(null);
|
|
313
|
+
const [selectedTaskId, setSelectedTaskId] = useState(null);
|
|
314
|
+
const [saving, setSaving] = useState(false);
|
|
315
|
+
const [deleting, setDeleting] = useState(false);
|
|
316
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
317
|
+
const [saveError, setSaveError] = useState(null);
|
|
318
|
+
const [aiProfiles, setAiProfiles] = useState([]);
|
|
319
|
+
const [profilesError, setProfilesError] = useState(null);
|
|
320
|
+
const [isCreateTaskDialogOpen, setIsCreateTaskDialogOpen] = useState(false);
|
|
321
|
+
const [newTaskTitle, setNewTaskTitle] = useState("");
|
|
322
|
+
const [newTaskDependencySourceId, setNewTaskDependencySourceId] = useState(null);
|
|
323
|
+
const [creatingTask, setCreatingTask] = useState(false);
|
|
324
|
+
const [isAddChildTaskDialogOpen, setIsAddChildTaskDialogOpen] = useState(false);
|
|
325
|
+
const [newChildTaskTitle, setNewChildTaskTitle] = useState("");
|
|
326
|
+
const [creatingChildTask, setCreatingChildTask] = useState(false);
|
|
327
|
+
const [dependencyPickerOpen, setDependencyPickerOpen] = useState(false);
|
|
328
|
+
const [isBasicSettingsExpanded, setIsBasicSettingsExpanded] = useState(false);
|
|
329
|
+
const [templateAgentId, setTemplateAgentId] = useState(null);
|
|
330
|
+
const [templateAgentLoading, setTemplateAgentLoading] = useState(false);
|
|
331
|
+
const [templateAgentResetting, setTemplateAgentResetting] = useState(false);
|
|
332
|
+
const [templateAgentError, setTemplateAgentError] = useState(null);
|
|
333
|
+
const autosaveTimeoutRef = useRef(null);
|
|
334
|
+
const lastPersistedSignatureRef = useRef(null);
|
|
335
|
+
const latestDraftSignatureRef = useRef(null);
|
|
336
|
+
const saveRunIdRef = useRef(0);
|
|
337
|
+
const draftTemplateRef = useRef(null);
|
|
338
|
+
const selectedTemplateIdRef = useRef(selectedTemplateId);
|
|
339
|
+
const selectedTaskIdRef = useRef(selectedTaskId);
|
|
340
|
+
const templateAgentLoadRunIdRef = useRef(0);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
let cancelled = false;
|
|
343
|
+
loadAiProfiles()
|
|
344
|
+
.then((items) => {
|
|
345
|
+
if (!cancelled) {
|
|
346
|
+
setAiProfiles(items || []);
|
|
347
|
+
setProfilesError(null);
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
.catch((loadError) => {
|
|
351
|
+
if (!cancelled) {
|
|
352
|
+
setAiProfiles([]);
|
|
353
|
+
setProfilesError(getAiProfilesErrorMessage(loadError));
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return () => {
|
|
357
|
+
cancelled = true;
|
|
358
|
+
};
|
|
359
|
+
}, []);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
draftTemplateRef.current = draftTemplate;
|
|
362
|
+
}, [draftTemplate]);
|
|
363
|
+
const selectTemplate = useCallback((template) => {
|
|
364
|
+
const clonedTemplate = template ? cloneTemplate(template) : null;
|
|
365
|
+
const preferredTaskId = clonedTemplate?.id &&
|
|
366
|
+
clonedTemplate.id === selectedTemplateIdRef.current
|
|
367
|
+
? selectedTaskIdRef.current
|
|
368
|
+
: null;
|
|
369
|
+
const persistedSignature = clonedTemplate && clonedTemplate.name.trim()
|
|
370
|
+
? JSON.stringify(buildUpsertTemplateRequest(clonedTemplate))
|
|
371
|
+
: null;
|
|
372
|
+
selectedTemplateIdRef.current = template?.id ?? null;
|
|
373
|
+
draftTemplateRef.current = clonedTemplate;
|
|
374
|
+
setSelectedTemplateId(template?.id ?? null);
|
|
375
|
+
setDraftTemplate(clonedTemplate);
|
|
376
|
+
setSelectedTaskId(getSelectedTaskIdForTemplate(clonedTemplate, preferredTaskId));
|
|
377
|
+
setIsDirty(false);
|
|
378
|
+
setSaveError(null);
|
|
379
|
+
lastPersistedSignatureRef.current = persistedSignature;
|
|
380
|
+
latestDraftSignatureRef.current = persistedSignature;
|
|
381
|
+
const nextId = template?.id ?? null;
|
|
382
|
+
const currentUrlId = readProjectTemplateIdFromUrl(searchParams);
|
|
383
|
+
if (nextId !== currentUrlId) {
|
|
384
|
+
editContext?.updateUrl({
|
|
385
|
+
[SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: nextId ?? undefined,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}, [editContext, searchParams]);
|
|
389
|
+
const loadTemplates = useCallback(async (preferredTemplateId) => {
|
|
390
|
+
try {
|
|
391
|
+
setState("loading");
|
|
392
|
+
setError(null);
|
|
393
|
+
const result = await getProjectTemplates({ includeDisabled: true });
|
|
394
|
+
if (result.type !== "success") {
|
|
395
|
+
throw new Error(result.summary || "Failed to load project templates");
|
|
396
|
+
}
|
|
397
|
+
const loadedTemplates = result.data ?? [];
|
|
398
|
+
setTemplates(loadedTemplates);
|
|
399
|
+
setState("success");
|
|
400
|
+
const preferredTemplateExists = !!preferredTemplateId &&
|
|
401
|
+
loadedTemplates.some((template) => template.id === preferredTemplateId);
|
|
402
|
+
const nextTemplateId = preferredTemplateExists
|
|
403
|
+
? preferredTemplateId
|
|
404
|
+
: preferredTemplateId
|
|
405
|
+
? null
|
|
406
|
+
: loadedTemplates[0]?.id || null;
|
|
407
|
+
const nextTemplate = loadedTemplates.find((template) => template.id === nextTemplateId);
|
|
408
|
+
selectTemplate(nextTemplate ?? null);
|
|
409
|
+
}
|
|
410
|
+
catch (loadError) {
|
|
411
|
+
setState("error");
|
|
412
|
+
setError(loadError instanceof Error
|
|
413
|
+
? loadError.message
|
|
414
|
+
: "Failed to load project templates");
|
|
415
|
+
}
|
|
416
|
+
}, [selectTemplate]);
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
void loadTemplates(readProjectTemplateIdFromWindow());
|
|
419
|
+
// Run once on mount. `loadTemplates` is recreated whenever `selectTemplate` changes
|
|
420
|
+
// (e.g. searchParams / editContext). Re-running on that identity change refetches from
|
|
421
|
+
// the server and calls selectTemplate, which resets the draft and drops unsaved edits
|
|
422
|
+
// before autosave can persist.
|
|
423
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional mount-only bootstrap
|
|
424
|
+
}, []);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (state !== "success" || !templates.length)
|
|
427
|
+
return;
|
|
428
|
+
const fromUrl = readProjectTemplateIdFromUrl(searchParams);
|
|
429
|
+
if (fromUrl === selectedTemplateId)
|
|
430
|
+
return;
|
|
431
|
+
if (!fromUrl && selectedTemplateId) {
|
|
432
|
+
editContext?.updateUrl({
|
|
433
|
+
[SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: selectedTemplateId,
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const match = fromUrl
|
|
438
|
+
? templates.find((template) => template.id === fromUrl)
|
|
439
|
+
: null;
|
|
440
|
+
if (fromUrl && !match) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
selectTemplate(match ?? null);
|
|
444
|
+
}, [
|
|
445
|
+
editContext,
|
|
446
|
+
searchParams,
|
|
447
|
+
selectTemplate,
|
|
448
|
+
selectedTemplateId,
|
|
449
|
+
state,
|
|
450
|
+
templates,
|
|
451
|
+
]);
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (!selectedTemplateId)
|
|
454
|
+
return;
|
|
455
|
+
const matchingTemplate = templates.find((template) => template.id === selectedTemplateId);
|
|
456
|
+
if (!matchingTemplate)
|
|
457
|
+
return;
|
|
458
|
+
if (draftTemplate?.id === matchingTemplate.id)
|
|
459
|
+
return;
|
|
460
|
+
setDraftTemplate(cloneTemplate(matchingTemplate));
|
|
461
|
+
setSelectedTaskId((currentSelectedTaskId) => getSelectedTaskIdForTemplate(matchingTemplate, currentSelectedTaskId));
|
|
462
|
+
setIsDirty(false);
|
|
463
|
+
}, [draftTemplate?.id, selectedTemplateId, templates]);
|
|
464
|
+
const aiProfilesById = useMemo(() => {
|
|
465
|
+
const byId = {};
|
|
466
|
+
for (const profile of aiProfiles) {
|
|
467
|
+
if (!profile.id)
|
|
468
|
+
continue;
|
|
469
|
+
byId[profile.id.toLowerCase()] = profile;
|
|
470
|
+
}
|
|
471
|
+
return byId;
|
|
472
|
+
}, [aiProfiles]);
|
|
473
|
+
const filteredTemplates = useMemo(() => {
|
|
474
|
+
const query = searchQuery.trim().toLowerCase();
|
|
475
|
+
if (!query)
|
|
476
|
+
return templates;
|
|
477
|
+
return templates.filter((template) => {
|
|
478
|
+
return (template.name.toLowerCase().includes(query) ||
|
|
479
|
+
template.description?.toLowerCase().includes(query) ||
|
|
480
|
+
template.taskTemplates.some((task) => task.title.toLowerCase().includes(query)));
|
|
481
|
+
});
|
|
482
|
+
}, [searchQuery, templates]);
|
|
483
|
+
const selectedTask = useMemo(() => draftTemplate?.taskTemplates.find((task) => task.id === selectedTaskId) ??
|
|
484
|
+
null, [draftTemplate, selectedTaskId]);
|
|
485
|
+
const dependencyTasks = useMemo(() => {
|
|
486
|
+
if (!draftTemplate || !selectedTask)
|
|
487
|
+
return [];
|
|
488
|
+
const byId = new Map(draftTemplate.taskTemplates.map((t) => [t.id, t]));
|
|
489
|
+
return selectedTask.dependencyIds
|
|
490
|
+
.map((id) => byId.get(id))
|
|
491
|
+
.filter((t) => t != null);
|
|
492
|
+
}, [draftTemplate, selectedTask]);
|
|
493
|
+
const childTaskTemplates = useMemo(() => {
|
|
494
|
+
if (!draftTemplate || !selectedTaskId)
|
|
495
|
+
return [];
|
|
496
|
+
return draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId && task.parentTaskId === selectedTaskId);
|
|
497
|
+
}, [draftTemplate, selectedTaskId]);
|
|
498
|
+
const dependencyAddCandidates = useMemo(() => {
|
|
499
|
+
if (!draftTemplate || !selectedTaskId || !selectedTask)
|
|
500
|
+
return [];
|
|
501
|
+
return draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId &&
|
|
502
|
+
!selectedTask.dependencyIds.includes(task.id));
|
|
503
|
+
}, [draftTemplate, selectedTaskId, selectedTask]);
|
|
504
|
+
const parentTaskSelectOptions = useMemo(() => {
|
|
505
|
+
if (!draftTemplate || !selectedTaskId) {
|
|
506
|
+
return [{ value: "", label: "None" }];
|
|
507
|
+
}
|
|
508
|
+
const others = draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId);
|
|
509
|
+
return [
|
|
510
|
+
{ value: "", label: "None" },
|
|
511
|
+
...[...others]
|
|
512
|
+
.sort((a, b) => (a.title || "").localeCompare(b.title || "", undefined, {
|
|
513
|
+
sensitivity: "base",
|
|
514
|
+
}))
|
|
515
|
+
.map((task) => ({
|
|
516
|
+
value: task.id,
|
|
517
|
+
label: task.title?.trim() || "Untitled Task",
|
|
518
|
+
})),
|
|
519
|
+
];
|
|
520
|
+
}, [draftTemplate, selectedTaskId]);
|
|
521
|
+
const headerStatusBadge = useMemo(() => {
|
|
522
|
+
if (saveError) {
|
|
523
|
+
return (_jsx("span", { "data-testid": "project-template-save-status", "data-state": "error", className: "inline-flex min-w-[84px] justify-center rounded bg-red-100 px-1.5 py-0.5 whitespace-nowrap text-red-700", children: "Save failed" }));
|
|
524
|
+
}
|
|
525
|
+
if (isDirty || saving) {
|
|
526
|
+
return (_jsx("span", { "data-testid": "project-template-save-status", "data-state": "saving", className: "inline-flex min-w-[84px] justify-center rounded bg-amber-100 px-1.5 py-0.5 whitespace-nowrap text-amber-700", children: "Saving..." }));
|
|
527
|
+
}
|
|
528
|
+
return (_jsx("span", { "aria-hidden": "true", "data-testid": "project-template-save-status", "data-state": "saved", className: "inline-flex min-w-[84px] justify-center rounded px-1.5 py-0.5 whitespace-nowrap opacity-0", children: "Saving..." }));
|
|
529
|
+
}, [isDirty, saveError, saving]);
|
|
530
|
+
const graphTasks = useMemo(() => buildTemplateTaskItems(draftTemplate, aiProfilesById), [aiProfilesById, draftTemplate]);
|
|
531
|
+
const graphDependencies = useMemo(() => buildTemplateDependencies(draftTemplate), [draftTemplate]);
|
|
532
|
+
const profileOptions = useMemo(() => aiProfiles.map((profile) => ({
|
|
533
|
+
value: profile.id,
|
|
534
|
+
label: profile.displayTitle || profile.name,
|
|
535
|
+
description: profile.description || undefined,
|
|
536
|
+
})), [aiProfiles]);
|
|
537
|
+
const syncProjectTemplateAgentContext = useCallback(async (agentId, template, selectedTaskTemplateId) => {
|
|
538
|
+
try {
|
|
539
|
+
await updateAgentContext(agentId, buildProjectTemplateAgentContext(template, selectedTaskTemplateId));
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
setTemplateAgentError(error instanceof Error
|
|
543
|
+
? `Failed to update template assistant context: ${error.message}`
|
|
544
|
+
: "Failed to update template assistant context");
|
|
545
|
+
}
|
|
546
|
+
}, []);
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
const runId = ++templateAgentLoadRunIdRef.current;
|
|
549
|
+
if (!selectedTemplateId) {
|
|
550
|
+
setTemplateAgentId(null);
|
|
551
|
+
setTemplateAgentLoading(false);
|
|
552
|
+
setTemplateAgentError(null);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const selectedTemplate = draftTemplateRef.current;
|
|
556
|
+
if (!selectedTemplate || selectedTemplate.id !== selectedTemplateId) {
|
|
557
|
+
setTemplateAgentId(null);
|
|
558
|
+
setTemplateAgentLoading(true);
|
|
559
|
+
setTemplateAgentError(null);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
setTemplateAgentId(null);
|
|
563
|
+
setTemplateAgentLoading(true);
|
|
564
|
+
setTemplateAgentError(null);
|
|
565
|
+
void getProjectTemplateAgent(selectedTemplateId)
|
|
566
|
+
.then(async (result) => {
|
|
567
|
+
if (runId !== templateAgentLoadRunIdRef.current) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (result.type !== "success" || !result.data) {
|
|
571
|
+
throw new Error(result.summary || "Failed to load project template assistant");
|
|
572
|
+
}
|
|
573
|
+
setTemplateAgentId(result.data.agentId);
|
|
574
|
+
setTemplateAgentLoading(false);
|
|
575
|
+
})
|
|
576
|
+
.catch((loadError) => {
|
|
577
|
+
if (runId !== templateAgentLoadRunIdRef.current)
|
|
578
|
+
return;
|
|
579
|
+
setTemplateAgentId(null);
|
|
580
|
+
setTemplateAgentLoading(false);
|
|
581
|
+
setTemplateAgentError(loadError instanceof Error
|
|
582
|
+
? loadError.message
|
|
583
|
+
: "Failed to load project template assistant");
|
|
584
|
+
});
|
|
585
|
+
}, [draftTemplate?.id, selectedTemplateId, syncProjectTemplateAgentContext]);
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
if (!templateAgentId || templateAgentLoading)
|
|
588
|
+
return;
|
|
589
|
+
const currentTemplate = draftTemplateRef.current;
|
|
590
|
+
if (!currentTemplate || currentTemplate.id !== selectedTemplateId)
|
|
591
|
+
return;
|
|
592
|
+
void syncProjectTemplateAgentContext(templateAgentId, currentTemplate, selectedTaskId);
|
|
593
|
+
}, [
|
|
594
|
+
selectedTaskId,
|
|
595
|
+
templateAgentId,
|
|
596
|
+
templateAgentLoading,
|
|
597
|
+
selectedTemplateId,
|
|
598
|
+
syncProjectTemplateAgentContext,
|
|
599
|
+
]);
|
|
600
|
+
const applyDraftChange = useCallback((updater) => {
|
|
601
|
+
setDraftTemplate((current) => {
|
|
602
|
+
if (!current)
|
|
603
|
+
return current;
|
|
604
|
+
const next = normalizeProjectTemplate(updater(cloneTemplate(current)));
|
|
605
|
+
const nextSignature = next.name.trim()
|
|
606
|
+
? JSON.stringify(buildUpsertTemplateRequest(next))
|
|
607
|
+
: null;
|
|
608
|
+
draftTemplateRef.current = next;
|
|
609
|
+
latestDraftSignatureRef.current = nextSignature;
|
|
610
|
+
setTemplates((existingTemplates) => existingTemplates.map((template) => template.id === next.id ? cloneTemplate(next) : template));
|
|
611
|
+
return next;
|
|
612
|
+
});
|
|
613
|
+
setIsDirty(true);
|
|
614
|
+
setSaveError(null);
|
|
615
|
+
}, []);
|
|
616
|
+
const updateSelectedTask = useCallback((updater) => {
|
|
617
|
+
if (!selectedTaskId)
|
|
618
|
+
return;
|
|
619
|
+
applyDraftChange((currentTemplate) => ({
|
|
620
|
+
...currentTemplate,
|
|
621
|
+
taskTemplates: currentTemplate.taskTemplates.map((task) => task.id === selectedTaskId ? updater({ ...task }) : task),
|
|
622
|
+
}));
|
|
623
|
+
}, [applyDraftChange, selectedTaskId]);
|
|
624
|
+
const currentSaveRequest = useMemo(() => {
|
|
625
|
+
if (!draftTemplate || !draftTemplate.name.trim()) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return buildUpsertTemplateRequest(draftTemplate);
|
|
629
|
+
}, [draftTemplate]);
|
|
630
|
+
const currentSaveSignature = useMemo(() => (currentSaveRequest ? JSON.stringify(currentSaveRequest) : null), [currentSaveRequest]);
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
latestDraftSignatureRef.current = currentSaveSignature;
|
|
633
|
+
}, [currentSaveSignature]);
|
|
634
|
+
const persistTemplate = useCallback(async (request, signature, options) => {
|
|
635
|
+
const runId = ++saveRunIdRef.current;
|
|
636
|
+
setSaving(true);
|
|
637
|
+
setSaveError(null);
|
|
638
|
+
try {
|
|
639
|
+
const result = await upsertProjectTemplate(request);
|
|
640
|
+
if (result.type !== "success" || !result.data) {
|
|
641
|
+
const message = result.summary || "Failed to save project template";
|
|
642
|
+
if (latestDraftSignatureRef.current === signature) {
|
|
643
|
+
setSaveError(message);
|
|
644
|
+
}
|
|
645
|
+
if (options?.showErrorToast) {
|
|
646
|
+
toast.error(message);
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const savedTemplate = mergeTemplateResponseWithRequest(result.data, request);
|
|
651
|
+
lastPersistedSignatureRef.current = signature;
|
|
652
|
+
setTemplates((existingTemplates) => sortTemplates([
|
|
653
|
+
...existingTemplates.filter((template) => template.id !== savedTemplate.id),
|
|
654
|
+
savedTemplate,
|
|
655
|
+
]));
|
|
656
|
+
if (latestDraftSignatureRef.current === signature) {
|
|
657
|
+
setDraftTemplate((current) => current?.id === savedTemplate.id
|
|
658
|
+
? cloneTemplate(savedTemplate)
|
|
659
|
+
: current);
|
|
660
|
+
if (selectedTemplateIdRef.current === savedTemplate.id) {
|
|
661
|
+
setSelectedTaskId((currentSelectedTaskId) => getSelectedTaskIdForTemplate(savedTemplate, currentSelectedTaskId));
|
|
662
|
+
}
|
|
663
|
+
setIsDirty(false);
|
|
664
|
+
setSaveError(null);
|
|
665
|
+
}
|
|
666
|
+
if (templateAgentId && savedTemplate.id === selectedTemplateId) {
|
|
667
|
+
const nextSelectedTaskId = getSelectedTaskIdForTemplate(savedTemplate, selectedTaskIdRef.current);
|
|
668
|
+
void syncProjectTemplateAgentContext(templateAgentId, savedTemplate, nextSelectedTaskId);
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
if (runId === saveRunIdRef.current) {
|
|
674
|
+
setSaving(false);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}, [selectedTemplateId, syncProjectTemplateAgentContext, templateAgentId]);
|
|
678
|
+
const flushPendingSave = useCallback(async (options) => {
|
|
679
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
680
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
681
|
+
autosaveTimeoutRef.current = null;
|
|
682
|
+
}
|
|
683
|
+
if (!currentSaveRequest || !currentSaveSignature) {
|
|
684
|
+
if (draftTemplate && !draftTemplate.name.trim()) {
|
|
685
|
+
const message = "Project template name is required before it can be saved";
|
|
686
|
+
setSaveError(message);
|
|
687
|
+
if (options?.showErrorToast) {
|
|
688
|
+
toast.error(message);
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
if (currentSaveSignature === lastPersistedSignatureRef.current) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
return persistTemplate(currentSaveRequest, currentSaveSignature, options);
|
|
698
|
+
}, [currentSaveRequest, currentSaveSignature, draftTemplate, persistTemplate]);
|
|
699
|
+
useEffect(() => {
|
|
700
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
701
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
702
|
+
autosaveTimeoutRef.current = null;
|
|
703
|
+
}
|
|
704
|
+
if (!draftTemplate) {
|
|
705
|
+
setIsDirty(false);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (saving) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (!currentSaveRequest || !currentSaveSignature) {
|
|
712
|
+
if (!draftTemplate.name.trim()) {
|
|
713
|
+
setIsDirty(true);
|
|
714
|
+
setSaveError("Project template name is required before it can be saved");
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (currentSaveSignature === lastPersistedSignatureRef.current) {
|
|
719
|
+
setIsDirty(false);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
setIsDirty(true);
|
|
723
|
+
autosaveTimeoutRef.current = window.setTimeout(() => {
|
|
724
|
+
autosaveTimeoutRef.current = null;
|
|
725
|
+
void persistTemplate(currentSaveRequest, currentSaveSignature);
|
|
726
|
+
}, PROJECT_TEMPLATE_AUTOSAVE_DEBOUNCE_MS);
|
|
727
|
+
return () => {
|
|
728
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
729
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
730
|
+
autosaveTimeoutRef.current = null;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}, [
|
|
734
|
+
currentSaveRequest,
|
|
735
|
+
currentSaveSignature,
|
|
736
|
+
draftTemplate,
|
|
737
|
+
persistTemplate,
|
|
738
|
+
saving,
|
|
739
|
+
]);
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
return () => {
|
|
742
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
743
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}, []);
|
|
747
|
+
const loadTemplatesRef = useRef(loadTemplates);
|
|
748
|
+
useEffect(() => {
|
|
749
|
+
loadTemplatesRef.current = loadTemplates;
|
|
750
|
+
}, [loadTemplates]);
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
selectedTemplateIdRef.current = selectedTemplateId;
|
|
753
|
+
}, [selectedTemplateId]);
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
selectedTaskIdRef.current = selectedTaskId;
|
|
756
|
+
}, [selectedTaskId]);
|
|
757
|
+
const handleCreateTemplate = useCallback(async () => {
|
|
758
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
759
|
+
if (!canContinue) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const nextTemplate = createEmptyTemplate();
|
|
763
|
+
selectedTemplateIdRef.current = nextTemplate.id;
|
|
764
|
+
draftTemplateRef.current = nextTemplate;
|
|
765
|
+
setTemplates((current) => sortTemplates([nextTemplate, ...current]));
|
|
766
|
+
setSelectedTemplateId(nextTemplate.id);
|
|
767
|
+
setDraftTemplate(cloneTemplate(nextTemplate));
|
|
768
|
+
setSelectedTaskId(null);
|
|
769
|
+
setIsDirty(true);
|
|
770
|
+
setSaveError(null);
|
|
771
|
+
lastPersistedSignatureRef.current = null;
|
|
772
|
+
latestDraftSignatureRef.current = null;
|
|
773
|
+
editContext?.updateUrl({
|
|
774
|
+
[SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: nextTemplate.id,
|
|
775
|
+
});
|
|
776
|
+
}, [editContext, flushPendingSave]);
|
|
777
|
+
const handleDeleteTemplate = useCallback(() => {
|
|
778
|
+
if (!draftTemplate)
|
|
779
|
+
return;
|
|
780
|
+
const templateId = draftTemplate.id;
|
|
781
|
+
const templateName = draftTemplate.name.trim() || "Untitled template";
|
|
782
|
+
const runDelete = async () => {
|
|
783
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
784
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
785
|
+
autosaveTimeoutRef.current = null;
|
|
786
|
+
}
|
|
787
|
+
const existsOnServer = templates.some((template) => template.id === templateId);
|
|
788
|
+
if (!existsOnServer) {
|
|
789
|
+
const remainingTemplates = templates.filter((template) => template.id !== templateId);
|
|
790
|
+
setTemplates(remainingTemplates);
|
|
791
|
+
selectTemplate(remainingTemplates[0] ?? null);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
setDeleting(true);
|
|
795
|
+
try {
|
|
796
|
+
const result = await deleteProjectTemplate(templateId);
|
|
797
|
+
if (result.type !== "success") {
|
|
798
|
+
toast.error(result.summary || "Failed to delete project template");
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
toast.success("Project template deleted");
|
|
802
|
+
const fallbackId = templates.find((template) => template.id !== templateId)?.id;
|
|
803
|
+
await loadTemplates(fallbackId ?? null);
|
|
804
|
+
}
|
|
805
|
+
finally {
|
|
806
|
+
setDeleting(false);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
if (editContext?.confirm) {
|
|
810
|
+
const existsOnServer = templates.some((template) => template.id === templateId);
|
|
811
|
+
editContext.confirm({
|
|
812
|
+
header: "Delete project template",
|
|
813
|
+
message: existsOnServer
|
|
814
|
+
? `Are you sure you want to delete "${templateName}"?\n\nThis action cannot be undone.`
|
|
815
|
+
: `Remove "${templateName}" from the list?\n\nThis template has not been saved yet.`,
|
|
816
|
+
acceptLabel: "Delete",
|
|
817
|
+
showCancel: true,
|
|
818
|
+
accept: () => {
|
|
819
|
+
void runDelete();
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
void runDelete();
|
|
825
|
+
}, [draftTemplate, editContext, loadTemplates, selectTemplate, templates]);
|
|
826
|
+
const handleSelectTemplate = useCallback(async (template) => {
|
|
827
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
828
|
+
if (!canContinue) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
selectTemplate(template);
|
|
832
|
+
}, [flushPendingSave, selectTemplate]);
|
|
833
|
+
const handleRefreshTemplates = useCallback(async () => {
|
|
834
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
835
|
+
if (!canContinue) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
await loadTemplates(selectedTemplateId);
|
|
839
|
+
}, [flushPendingSave, loadTemplates, selectedTemplateId]);
|
|
840
|
+
const handleResetTemplateAgent = useCallback(async () => {
|
|
841
|
+
if (!selectedTemplateId)
|
|
842
|
+
return;
|
|
843
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
844
|
+
if (!canContinue) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
setTemplateAgentResetting(true);
|
|
848
|
+
setTemplateAgentError(null);
|
|
849
|
+
try {
|
|
850
|
+
const result = await resetProjectTemplateAgent(selectedTemplateId);
|
|
851
|
+
if (result.type !== "success" || !result.data) {
|
|
852
|
+
const message = result.summary || "Failed to restart the template assistant";
|
|
853
|
+
setTemplateAgentError(message);
|
|
854
|
+
toast.error(message);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
setTemplateAgentId(result.data.agentId);
|
|
858
|
+
const currentTemplate = draftTemplateRef.current;
|
|
859
|
+
if (currentTemplate?.id === selectedTemplateId) {
|
|
860
|
+
await syncProjectTemplateAgentContext(result.data.agentId, currentTemplate, selectedTaskIdRef.current);
|
|
861
|
+
}
|
|
862
|
+
toast.success("Template assistant restarted");
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
const message = error instanceof Error
|
|
866
|
+
? error.message
|
|
867
|
+
: "Failed to restart the template assistant";
|
|
868
|
+
setTemplateAgentError(message);
|
|
869
|
+
toast.error(message);
|
|
870
|
+
}
|
|
871
|
+
finally {
|
|
872
|
+
setTemplateAgentResetting(false);
|
|
873
|
+
}
|
|
874
|
+
}, [flushPendingSave, selectedTemplateId, syncProjectTemplateAgentContext]);
|
|
875
|
+
const handleOpenCreateTaskDialog = useCallback(async () => {
|
|
876
|
+
if (!draftTemplate)
|
|
877
|
+
return;
|
|
878
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
879
|
+
if (!canContinue) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
setNewTaskDependencySourceId(null);
|
|
883
|
+
setNewTaskTitle("");
|
|
884
|
+
setIsCreateTaskDialogOpen(true);
|
|
885
|
+
}, [draftTemplate, flushPendingSave]);
|
|
886
|
+
const handleOpenCreateDependentTaskDialogForTask = useCallback(async (taskId) => {
|
|
887
|
+
if (!draftTemplate)
|
|
888
|
+
return;
|
|
889
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
890
|
+
if (!canContinue) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
setSelectedTaskId(taskId);
|
|
894
|
+
setNewTaskDependencySourceId(taskId);
|
|
895
|
+
setNewTaskTitle("");
|
|
896
|
+
setIsCreateTaskDialogOpen(true);
|
|
897
|
+
}, [draftTemplate, flushPendingSave]);
|
|
898
|
+
const handleOpenAddChildTaskDialogForTask = useCallback(async (taskId) => {
|
|
899
|
+
if (!draftTemplate)
|
|
900
|
+
return;
|
|
901
|
+
const canContinue = await flushPendingSave({ showErrorToast: true });
|
|
902
|
+
if (!canContinue) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
setSelectedTaskId(taskId);
|
|
906
|
+
setNewChildTaskTitle("");
|
|
907
|
+
setIsAddChildTaskDialogOpen(true);
|
|
908
|
+
}, [draftTemplate, flushPendingSave]);
|
|
909
|
+
const handleConfirmCreateTask = useCallback(async () => {
|
|
910
|
+
if (!draftTemplate)
|
|
911
|
+
return;
|
|
912
|
+
const trimmedTitle = newTaskTitle.trim();
|
|
913
|
+
if (!trimmedTitle) {
|
|
914
|
+
toast.error("Task title is required");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (!draftTemplate.name.trim()) {
|
|
918
|
+
const message = "Project template name is required before it can be saved";
|
|
919
|
+
setSaveError(message);
|
|
920
|
+
toast.error(message);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
924
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
925
|
+
autosaveTimeoutRef.current = null;
|
|
926
|
+
}
|
|
927
|
+
const nextTask = createEmptyTaskTemplate(draftTemplate.taskTemplates.length, trimmedTitle);
|
|
928
|
+
if (newTaskDependencySourceId) {
|
|
929
|
+
nextTask.dependencyIds = [newTaskDependencySourceId];
|
|
930
|
+
}
|
|
931
|
+
const nextTemplate = cloneTemplate(draftTemplate);
|
|
932
|
+
nextTemplate.taskTemplates = [...nextTemplate.taskTemplates, nextTask];
|
|
933
|
+
const request = buildUpsertTemplateRequest(nextTemplate);
|
|
934
|
+
const signature = JSON.stringify(request);
|
|
935
|
+
latestDraftSignatureRef.current = signature;
|
|
936
|
+
setDraftTemplate(cloneTemplate(nextTemplate));
|
|
937
|
+
setTemplates((existingTemplates) => sortTemplates([
|
|
938
|
+
...existingTemplates.filter((template) => template.id !== nextTemplate.id),
|
|
939
|
+
cloneTemplate(nextTemplate),
|
|
940
|
+
]));
|
|
941
|
+
setSelectedTaskId(nextTask.id);
|
|
942
|
+
setIsDirty(true);
|
|
943
|
+
setSaveError(null);
|
|
944
|
+
setCreatingTask(true);
|
|
945
|
+
try {
|
|
946
|
+
const saved = await persistTemplate(request, signature, {
|
|
947
|
+
showErrorToast: true,
|
|
948
|
+
});
|
|
949
|
+
if (saved) {
|
|
950
|
+
setIsCreateTaskDialogOpen(false);
|
|
951
|
+
setNewTaskDependencySourceId(null);
|
|
952
|
+
setNewTaskTitle("");
|
|
953
|
+
toast.success(newTaskDependencySourceId
|
|
954
|
+
? "Dependent task template created"
|
|
955
|
+
: "Task template created");
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
setCreatingTask(false);
|
|
960
|
+
}
|
|
961
|
+
}, [
|
|
962
|
+
draftTemplate,
|
|
963
|
+
newTaskDependencySourceId,
|
|
964
|
+
newTaskTitle,
|
|
965
|
+
persistTemplate,
|
|
966
|
+
]);
|
|
967
|
+
const handleOpenAddChildTaskDialog = useCallback(async () => {
|
|
968
|
+
if (!draftTemplate || !selectedTaskId)
|
|
969
|
+
return;
|
|
970
|
+
await handleOpenAddChildTaskDialogForTask(selectedTaskId);
|
|
971
|
+
}, [draftTemplate, handleOpenAddChildTaskDialogForTask, selectedTaskId]);
|
|
972
|
+
const handleConfirmCreateChildTask = useCallback(async () => {
|
|
973
|
+
if (!draftTemplate || !selectedTaskId)
|
|
974
|
+
return;
|
|
975
|
+
const trimmedTitle = newChildTaskTitle.trim();
|
|
976
|
+
if (!trimmedTitle) {
|
|
977
|
+
toast.error("Task title is required");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (!draftTemplate.name.trim()) {
|
|
981
|
+
const message = "Project template name is required before it can be saved";
|
|
982
|
+
setSaveError(message);
|
|
983
|
+
toast.error(message);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (autosaveTimeoutRef.current !== null) {
|
|
987
|
+
window.clearTimeout(autosaveTimeoutRef.current);
|
|
988
|
+
autosaveTimeoutRef.current = null;
|
|
989
|
+
}
|
|
990
|
+
const nextTask = createEmptyTaskTemplate(draftTemplate.taskTemplates.length, trimmedTitle);
|
|
991
|
+
nextTask.parentTaskId = selectedTaskId;
|
|
992
|
+
nextTask.dependencyIds = [selectedTaskId];
|
|
993
|
+
const nextTemplate = cloneTemplate(draftTemplate);
|
|
994
|
+
nextTemplate.taskTemplates = [...nextTemplate.taskTemplates, nextTask];
|
|
995
|
+
const request = buildUpsertTemplateRequest(nextTemplate);
|
|
996
|
+
const signature = JSON.stringify(request);
|
|
997
|
+
latestDraftSignatureRef.current = signature;
|
|
998
|
+
setDraftTemplate(cloneTemplate(nextTemplate));
|
|
999
|
+
setTemplates((existingTemplates) => sortTemplates([
|
|
1000
|
+
...existingTemplates.filter((template) => template.id !== nextTemplate.id),
|
|
1001
|
+
cloneTemplate(nextTemplate),
|
|
1002
|
+
]));
|
|
1003
|
+
// Keep the parent task selected so the new child appears in "Child tasks" immediately.
|
|
1004
|
+
setIsDirty(true);
|
|
1005
|
+
setSaveError(null);
|
|
1006
|
+
setCreatingChildTask(true);
|
|
1007
|
+
try {
|
|
1008
|
+
const saved = await persistTemplate(request, signature, {
|
|
1009
|
+
showErrorToast: true,
|
|
1010
|
+
});
|
|
1011
|
+
if (saved) {
|
|
1012
|
+
setIsAddChildTaskDialogOpen(false);
|
|
1013
|
+
setNewChildTaskTitle("");
|
|
1014
|
+
toast.success("Child task template created");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
setCreatingChildTask(false);
|
|
1019
|
+
}
|
|
1020
|
+
}, [draftTemplate, newChildTaskTitle, persistTemplate, selectedTaskId]);
|
|
1021
|
+
const handleRemoveDependency = useCallback((dependencyId) => {
|
|
1022
|
+
updateSelectedTask((currentTask) => ({
|
|
1023
|
+
...currentTask,
|
|
1024
|
+
parentTaskId: currentTask.parentTaskId === dependencyId
|
|
1025
|
+
? null
|
|
1026
|
+
: currentTask.parentTaskId,
|
|
1027
|
+
dependencyIds: currentTask.dependencyIds.filter((id) => id !== dependencyId),
|
|
1028
|
+
}));
|
|
1029
|
+
}, [updateSelectedTask]);
|
|
1030
|
+
const handleAddDependencyToTask = useCallback((taskId, dependencyId) => {
|
|
1031
|
+
const currentTemplate = draftTemplateRef.current;
|
|
1032
|
+
if (!canAddTemplateDependency(currentTemplate, taskId, dependencyId)) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
applyDraftChange((template) => ({
|
|
1036
|
+
...template,
|
|
1037
|
+
taskTemplates: template.taskTemplates.map((task) => task.id === taskId
|
|
1038
|
+
? {
|
|
1039
|
+
...task,
|
|
1040
|
+
dependencyIds: Array.from(new Set([...(task.dependencyIds ?? []), dependencyId])),
|
|
1041
|
+
}
|
|
1042
|
+
: task),
|
|
1043
|
+
}));
|
|
1044
|
+
return true;
|
|
1045
|
+
}, [applyDraftChange]);
|
|
1046
|
+
const handleAddDependency = useCallback((dependencyId) => {
|
|
1047
|
+
if (!selectedTaskId)
|
|
1048
|
+
return;
|
|
1049
|
+
handleAddDependencyToTask(selectedTaskId, dependencyId);
|
|
1050
|
+
setDependencyPickerOpen(false);
|
|
1051
|
+
}, [handleAddDependencyToTask, selectedTaskId]);
|
|
1052
|
+
const handleCreateDependencyFromGraph = useCallback((dependsOnTaskId, taskId) => {
|
|
1053
|
+
const didAddDependency = handleAddDependencyToTask(taskId, dependsOnTaskId);
|
|
1054
|
+
if (!didAddDependency) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
setSelectedTaskId(taskId);
|
|
1058
|
+
}, [handleAddDependencyToTask]);
|
|
1059
|
+
const handleCreateChildRelationshipFromGraph = useCallback((parentTaskId, childTaskId) => {
|
|
1060
|
+
const currentTemplate = draftTemplateRef.current;
|
|
1061
|
+
const taskTemplates = currentTemplate?.taskTemplates ?? [];
|
|
1062
|
+
const childTask = taskTemplates.find((task) => task.id === childTaskId);
|
|
1063
|
+
if (!childTask || childTaskId === parentTaskId) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const parentByTaskId = new Map(taskTemplates.map((task) => [task.id, task.parentTaskId ?? null]));
|
|
1067
|
+
let ancestorTaskId = parentByTaskId.get(parentTaskId) ?? null;
|
|
1068
|
+
while (ancestorTaskId) {
|
|
1069
|
+
if (ancestorTaskId === childTaskId) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
ancestorTaskId = parentByTaskId.get(ancestorTaskId) ?? null;
|
|
1073
|
+
}
|
|
1074
|
+
applyDraftChange((template) => ({
|
|
1075
|
+
...template,
|
|
1076
|
+
taskTemplates: template.taskTemplates.map((task) => {
|
|
1077
|
+
if (task.id !== childTaskId) {
|
|
1078
|
+
return task;
|
|
1079
|
+
}
|
|
1080
|
+
const existingParentTaskId = task.parentTaskId ?? null;
|
|
1081
|
+
return {
|
|
1082
|
+
...task,
|
|
1083
|
+
parentTaskId,
|
|
1084
|
+
dependencyIds: Array.from(new Set([
|
|
1085
|
+
...task.dependencyIds.filter((id) => id !== existingParentTaskId),
|
|
1086
|
+
parentTaskId,
|
|
1087
|
+
])),
|
|
1088
|
+
};
|
|
1089
|
+
}),
|
|
1090
|
+
}));
|
|
1091
|
+
setSelectedTaskId(childTaskId);
|
|
1092
|
+
}, [applyDraftChange]);
|
|
1093
|
+
const handleUnlinkChildTask = useCallback((childTaskId) => {
|
|
1094
|
+
if (!selectedTaskId)
|
|
1095
|
+
return;
|
|
1096
|
+
applyDraftChange((currentTemplate) => ({
|
|
1097
|
+
...currentTemplate,
|
|
1098
|
+
taskTemplates: currentTemplate.taskTemplates.map((task) => task.id === childTaskId
|
|
1099
|
+
? {
|
|
1100
|
+
...task,
|
|
1101
|
+
parentTaskId: null,
|
|
1102
|
+
dependencyIds: task.dependencyIds.filter((id) => id !== selectedTaskId),
|
|
1103
|
+
}
|
|
1104
|
+
: task),
|
|
1105
|
+
}));
|
|
1106
|
+
}, [applyDraftChange, selectedTaskId]);
|
|
1107
|
+
const removeTaskTemplateById = useCallback((taskId) => {
|
|
1108
|
+
const template = draftTemplateRef.current;
|
|
1109
|
+
if (!template?.taskTemplates.some((t) => t.id === taskId))
|
|
1110
|
+
return;
|
|
1111
|
+
const taskTitle = template.taskTemplates.find((t) => t.id === taskId)?.title ??
|
|
1112
|
+
"Untitled task";
|
|
1113
|
+
const runDelete = () => {
|
|
1114
|
+
const current = draftTemplateRef.current;
|
|
1115
|
+
if (!current?.taskTemplates.some((t) => t.id === taskId))
|
|
1116
|
+
return;
|
|
1117
|
+
const remainingTasks = current.taskTemplates.filter((task) => task.id !== taskId);
|
|
1118
|
+
applyDraftChange((currentTemplate) => ({
|
|
1119
|
+
...currentTemplate,
|
|
1120
|
+
graphLayout: currentTemplate.graphLayout
|
|
1121
|
+
? {
|
|
1122
|
+
...currentTemplate.graphLayout,
|
|
1123
|
+
nodes: currentTemplate.graphLayout.nodes.filter((node) => node.taskId !== taskId),
|
|
1124
|
+
}
|
|
1125
|
+
: null,
|
|
1126
|
+
taskTemplates: currentTemplate.taskTemplates
|
|
1127
|
+
.filter((task) => task.id !== taskId)
|
|
1128
|
+
.map((task) => ({
|
|
1129
|
+
...task,
|
|
1130
|
+
parentTaskId: task.parentTaskId === taskId ? null : task.parentTaskId,
|
|
1131
|
+
dependencyIds: task.dependencyIds.filter((id) => id !== taskId),
|
|
1132
|
+
})),
|
|
1133
|
+
}));
|
|
1134
|
+
setSelectedTaskId((prev) => {
|
|
1135
|
+
if (prev === taskId) {
|
|
1136
|
+
return remainingTasks[0]?.id ?? null;
|
|
1137
|
+
}
|
|
1138
|
+
return remainingTasks.some((t) => t.id === prev)
|
|
1139
|
+
? prev
|
|
1140
|
+
: (remainingTasks[0]?.id ?? null);
|
|
1141
|
+
});
|
|
1142
|
+
};
|
|
1143
|
+
if (editContext?.confirm) {
|
|
1144
|
+
editContext.confirm({
|
|
1145
|
+
header: "Remove task template",
|
|
1146
|
+
message: `Are you sure you want to remove "${taskTitle}"?\n\nOther tasks will no longer depend on this task.`,
|
|
1147
|
+
acceptLabel: "Remove",
|
|
1148
|
+
showCancel: true,
|
|
1149
|
+
accept: runDelete,
|
|
1150
|
+
});
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
runDelete();
|
|
1154
|
+
}, [applyDraftChange, editContext]);
|
|
1155
|
+
const handleDeleteSelectedTask = useCallback(() => {
|
|
1156
|
+
if (!selectedTaskId)
|
|
1157
|
+
return;
|
|
1158
|
+
removeTaskTemplateById(selectedTaskId);
|
|
1159
|
+
}, [removeTaskTemplateById, selectedTaskId]);
|
|
1160
|
+
const handleProjectTemplateEditorKeyDownCapture = useCallback((event) => {
|
|
1161
|
+
if (event.key !== "Delete")
|
|
1162
|
+
return;
|
|
1163
|
+
if (!selectedTaskId)
|
|
1164
|
+
return;
|
|
1165
|
+
if (isTypingEventTarget(event.nativeEvent))
|
|
1166
|
+
return;
|
|
1167
|
+
event.preventDefault();
|
|
1168
|
+
event.stopPropagation();
|
|
1169
|
+
handleDeleteSelectedTask();
|
|
1170
|
+
}, [handleDeleteSelectedTask, selectedTaskId]);
|
|
1171
|
+
const listContent = (_jsx("div", { className: "h-full overflow-auto p-4", "data-testid": "project-template-list-pane", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [_jsxs("div", { className: "shrink-0 space-y-4 pb-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: "flex min-h-5 items-center gap-2 text-xs text-gray-700", children: state === "loading" ? (_jsx(RefreshCw, { className: "h-4 w-4 animate-spin text-amber-600", strokeWidth: 1.5 })) : state === "error" ? (_jsx(AlertCircle, { className: "h-4 w-4 text-red-600", strokeWidth: 1.5 })) : null }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", onClick: () => void handleRefreshTemplates(), disabled: state === "loading" || deleting, "data-testid": "project-template-refresh-button", children: _jsx(RefreshCw, { className: cn("h-4 w-4", state === "loading" && "animate-spin"), strokeWidth: 1.5 }) }), _jsxs(Button, { size: "sm", onClick: () => void handleCreateTemplate(), "data-testid": "project-template-create-button", children: [_jsx(Plus, { className: "h-4 w-4", strokeWidth: 1.5 }), "Create Template"] })] })] }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400", strokeWidth: 1.5 }), _jsx(Input, { type: "text", placeholder: "Search templates...", value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), className: "pl-9 text-xs md:text-xs", "data-testid": "project-template-search-input" })] }), error && (_jsx("div", { className: "rounded border border-red-200 bg-red-50 p-2 text-xs whitespace-pre-wrap text-red-700", children: error }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: filteredTemplates.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx(GitBranch, { className: "mb-4 h-10 w-10 text-gray-300", strokeWidth: 1.2 }), _jsx("p", { className: "text-xs text-gray-500", children: searchQuery
|
|
1172
|
+
? "No templates match your search"
|
|
1173
|
+
: "No project templates found" })] })) : (_jsx("div", { className: "space-y-3", children: filteredTemplates.map((template) => {
|
|
1174
|
+
const isSelected = selectedTemplateId === template.id;
|
|
1175
|
+
return (_jsx("button", { type: "button", onClick: () => void handleSelectTemplate(template), "data-testid": `project-template-list-item-${template.id}`, className: cn("w-full rounded-lg border p-4 text-left transition-shadow hover:shadow-sm", isSelected
|
|
1176
|
+
? "border-blue-400 bg-blue-50"
|
|
1177
|
+
: "border-gray-200 bg-white"), children: _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [template.isSystem && (_jsx("span", { title: "System template", className: "inline-flex", children: _jsx(Shield, { className: "h-3.5 w-3.5 shrink-0 text-amber-600", strokeWidth: 1.5, "aria-label": "System template" }) })), _jsx("h4", { className: "font-medium text-gray-900", children: template.name }), template.disabled && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Disabled" })), template.hideFromCreateDialog && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Hidden" })), template.openInWizardMode && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Wizard" }))] }), template.description && (_jsx("div", { className: "mt-2 line-clamp-2 text-xs text-gray-600", children: template.description })), _jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2 text-xs text-gray-500", children: [_jsxs("span", { children: [template.taskTemplates.length, " task templates"] }), template.defaultCostLimit != null && (_jsxs("span", { children: ["Default budget: ", template.defaultCostLimit] }))] })] }) }, template.id));
|
|
1178
|
+
}) })) })] }) }));
|
|
1179
|
+
const detailContent = draftTemplate ? (_jsxs("div", { className: "flex h-full flex-col bg-gray-50/40", "data-testid": "project-template-detail-pane", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-gray-200 bg-white px-5 py-3.5", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 text-white", children: _jsx(GitBranch, { className: "h-4 w-4", strokeWidth: 1.5 }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-xs font-semibold text-gray-900", children: draftTemplate.name || "Project Template" }), _jsxs("div", { className: "mt-0.5 flex min-h-[20px] items-center gap-2 text-[11px] text-gray-500", children: [_jsxs("span", { children: [draftTemplate.taskTemplates.length, " task templates"] }), headerStatusBadge] })] })] }) }), _jsx("div", { className: "flex shrink-0 items-center gap-2", children: _jsxs(Button, { variant: "outline", size: "sm", onClick: handleDeleteTemplate, disabled: deleting, "data-testid": "project-template-delete-button", children: [_jsx(Trash2, { className: "h-4 w-4", strokeWidth: 1.5 }), deleting ? "Deleting..." : "Delete"] }) })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-5 overflow-hidden p-5", children: [saveError && (_jsx("div", { className: "rounded border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700", children: saveError })), _jsxs("section", { className: "shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm", children: [_jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-3 px-4 py-3 text-left", onClick: () => setIsBasicSettingsExpanded((currentValue) => !currentValue), "aria-expanded": isBasicSettingsExpanded, "aria-controls": "project-template-basic-settings", "data-testid": "project-template-basic-settings-toggle", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-500", children: _jsx(Settings2, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xs font-semibold tracking-wide text-gray-800", children: "Basic Settings" }), _jsx("p", { className: "text-[11px] leading-tight text-gray-400", children: "Configure the shared defaults for this project template." })] })] }), _jsx(ChevronDown, { className: cn("h-4 w-4 shrink-0 text-gray-400 transition-transform", isBasicSettingsExpanded ? "rotate-180" : "rotate-0"), strokeWidth: 1.5 })] }), isBasicSettingsExpanded && (_jsxs("div", { id: "project-template-basic-settings", className: "grid gap-4 border-t border-gray-100 p-4 md:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Template name" }), _jsx(Input, { value: draftTemplate.name, onChange: (event) => applyDraftChange((currentTemplate) => ({
|
|
1180
|
+
...currentTemplate,
|
|
1181
|
+
name: event.target.value,
|
|
1182
|
+
})), placeholder: "Project template name", className: "text-xs md:text-xs", "data-testid": "project-template-name-input" })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Description" }), _jsx(Textarea, { className: "text-xs", value: draftTemplate.description ?? "", onChange: (event) => applyDraftChange((currentTemplate) => ({
|
|
1183
|
+
...currentTemplate,
|
|
1184
|
+
description: event.target.value,
|
|
1185
|
+
})), rows: 4, placeholder: "Describe when to use this template", "data-testid": "project-template-description-input" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Default cost limit" }), _jsx(Input, { type: "number", value: draftTemplate.defaultCostLimit ?? "", onChange: (event) => applyDraftChange((currentTemplate) => ({
|
|
1186
|
+
...currentTemplate,
|
|
1187
|
+
defaultCostLimit: event.target.value.trim() === ""
|
|
1188
|
+
? null
|
|
1189
|
+
: Number(event.target.value),
|
|
1190
|
+
})), placeholder: "0", className: "text-xs md:text-xs", "data-testid": "project-template-default-cost-limit-input" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Disabled" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "Makes the template unavailable for project creation and template resolution." })] }), _jsx(Switch, { checked: draftTemplate.disabled === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
|
|
1191
|
+
...currentTemplate,
|
|
1192
|
+
disabled: checked,
|
|
1193
|
+
})), className: "data-[state=checked]:bg-amber-600 data-[state=checked]:shadow-inner dark:data-[state=checked]:bg-amber-600", "aria-label": "Disable this project template", "data-testid": "project-template-disabled-switch" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Hide from create dialog" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "Keeps the template active, but removes it from the create project dialog." })] }), _jsx(Switch, { checked: draftTemplate.hideFromCreateDialog === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
|
|
1194
|
+
...currentTemplate,
|
|
1195
|
+
hideFromCreateDialog: checked,
|
|
1196
|
+
})), "aria-label": "Hide this project template from the create dialog", "data-testid": "project-template-hide-from-create-dialog-switch" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Open in wizard mode" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "New projects created from this template start in taskboard wizard mode." })] }), _jsx(Switch, { checked: draftTemplate.openInWizardMode === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
|
|
1197
|
+
...currentTemplate,
|
|
1198
|
+
openInWizardMode: checked,
|
|
1199
|
+
})), "aria-label": "Open new projects from this template in wizard mode", "data-testid": "project-template-open-in-wizard-mode-switch" })] })] }))] }), _jsxs("section", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-gray-100 px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-500", children: _jsx(GitBranch, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xs font-semibold tracking-wide text-gray-800", children: "Template Task Editor" }), _jsx("p", { className: "text-[11px] leading-tight text-gray-400", children: "Add task templates, arrange them in the graph, and define dependencies." })] })] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => void handleOpenCreateTaskDialog(), disabled: creatingTask, "data-testid": "project-template-add-task-button", children: [_jsx(Plus, { className: "h-4 w-4", strokeWidth: 1.5 }), "Add Task"] })] }), _jsx("div", { className: "min-h-[360px] flex-1 overflow-hidden border-b border-gray-100", children: _jsx(Splitter, { direction: "vertical", localStorageKey: "settings-project-template-graph-task-splitter-v1", className: "h-full", panels: [
|
|
1200
|
+
{
|
|
1201
|
+
name: "project-template-graph",
|
|
1202
|
+
defaultSize: 400,
|
|
1203
|
+
className: "min-h-0",
|
|
1204
|
+
content: (_jsx("div", { className: "h-full min-h-0 overflow-hidden bg-slate-50/40", "data-testid": "project-template-graph", children: _jsx(DependencyGraphView, { projectId: draftTemplate.id, layoutKey: `project-template:${draftTemplate.id}`, tasks: graphTasks, dependencies: graphDependencies, miniMapWidth: 112, miniMapHeight: 84, orientation: "horizontal", autoLayoutStrategy: "hierarchy", savedLayout: draftTemplate.graphLayout, selectedTaskId: selectedTaskId, onSelectTask: setSelectedTaskId, onAddDependentTaskFromNode: (taskId) => {
|
|
1205
|
+
void handleOpenCreateDependentTaskDialogForTask(taskId);
|
|
1206
|
+
}, onAddChildTaskFromNode: (taskId) => {
|
|
1207
|
+
void handleOpenAddChildTaskDialogForTask(taskId);
|
|
1208
|
+
}, onRemoveTask: removeTaskTemplateById, allowDependencyConnect: true, onCreateDependency: handleCreateDependencyFromGraph, onCreateChildRelationship: handleCreateChildRelationshipFromGraph, canPersistLayout: true, layoutSaveDebounceMs: 0, highlightBlockedTasks: false, showExecutionStateBadges: false, emptyStateTitle: "No task templates yet", emptyStateDescription: "Add task templates to start designing the project flow.", onPersistLayout: async (layout) => {
|
|
1209
|
+
const nextLayout = {
|
|
1210
|
+
...layout,
|
|
1211
|
+
projectTemplateId: draftTemplate.id,
|
|
1212
|
+
};
|
|
1213
|
+
applyDraftChange((currentTemplate) => ({
|
|
1214
|
+
...currentTemplate,
|
|
1215
|
+
graphLayout: nextLayout,
|
|
1216
|
+
}));
|
|
1217
|
+
return nextLayout;
|
|
1218
|
+
} }) })),
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
name: "project-template-task-details",
|
|
1222
|
+
defaultSize: "auto",
|
|
1223
|
+
className: "min-h-0",
|
|
1224
|
+
content: (_jsx("div", { className: "flex h-full min-h-0 flex-col overflow-hidden", "data-testid": "project-template-task-detail-pane", children: !selectedTask ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: _jsx("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-xs text-gray-500", children: "Select a task node to edit its details." }) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "shrink-0 border-b border-gray-100 bg-white px-4 pb-3 pt-4", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs font-semibold text-gray-900", "data-testid": "project-template-selected-task-title", children: selectedTask.title || "Untitled Task" }), _jsx("div", { className: "text-xs text-gray-500", children: "Configure task details, assignment, and dependencies." })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: handleDeleteSelectedTask, "data-testid": "project-template-remove-task-button", children: [_jsx(Trash2, { className: "h-4 w-4", strokeWidth: 1.5 }), "Remove Task"] })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 pb-4 pt-3", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Task title" }), _jsx(Input, { value: selectedTask.title, onChange: (event) => updateSelectedTask((currentTask) => ({
|
|
1225
|
+
...currentTask,
|
|
1226
|
+
title: event.target.value,
|
|
1227
|
+
})), placeholder: "Task title", className: "text-xs md:text-xs", "data-testid": "project-template-task-title-input" })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Parent task" }), _jsx(Select, { className: "text-xs", value: selectedTask.parentTaskId ?? "", onValueChange: (value) => {
|
|
1228
|
+
const newParentId = value.trim() || null;
|
|
1229
|
+
updateSelectedTask((currentTask) => {
|
|
1230
|
+
const oldParent = currentTask.parentTaskId ?? null;
|
|
1231
|
+
let deps = [
|
|
1232
|
+
...(currentTask.dependencyIds ?? []),
|
|
1233
|
+
];
|
|
1234
|
+
if (oldParent &&
|
|
1235
|
+
oldParent !== newParentId) {
|
|
1236
|
+
deps = deps.filter((id) => id !== oldParent);
|
|
1237
|
+
}
|
|
1238
|
+
if (newParentId) {
|
|
1239
|
+
if (!deps.includes(newParentId)) {
|
|
1240
|
+
deps = [...deps, newParentId];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else if (oldParent) {
|
|
1244
|
+
deps = deps.filter((id) => id !== oldParent);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
...currentTask,
|
|
1248
|
+
parentTaskId: newParentId,
|
|
1249
|
+
dependencyIds: deps,
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1252
|
+
}, options: parentTaskSelectOptions, placeholder: "No parent", "data-testid": "project-template-task-parent-select" }), _jsx("p", { className: "text-[11px] leading-snug text-gray-500", children: "Marks this task as a child in the template; the parent is also added as a blocking dependency unless already listed." })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Description" }), _jsx(Textarea, { className: "text-xs", value: selectedTask.description ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
|
|
1253
|
+
...currentTask,
|
|
1254
|
+
description: event.target.value,
|
|
1255
|
+
})), rows: 4, placeholder: "Describe what this task should accomplish", "data-testid": "project-template-task-description-input" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Priority" }), _jsx(Select, { className: "text-xs", value: selectedTask.priority ?? "", onValueChange: (value) => updateSelectedTask((currentTask) => ({
|
|
1256
|
+
...currentTask,
|
|
1257
|
+
priority: value || null,
|
|
1258
|
+
})), options: PRIORITY_OPTIONS, "data-testid": "project-template-task-priority-select" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Default assignment type" }), _jsx(Select, { className: "text-xs", value: selectedTask.assigneeType ?? "", onValueChange: (value) => updateSelectedTask((currentTask) => ({
|
|
1259
|
+
...currentTask,
|
|
1260
|
+
assigneeType: (value ||
|
|
1261
|
+
null),
|
|
1262
|
+
assigneeId: value
|
|
1263
|
+
? (currentTask.assigneeId ?? null)
|
|
1264
|
+
: null,
|
|
1265
|
+
agentProfileId: value === "Agent"
|
|
1266
|
+
? (currentTask.agentProfileId ?? null)
|
|
1267
|
+
: null,
|
|
1268
|
+
})), options: ASSIGNEE_TYPE_OPTIONS, "data-testid": "project-template-task-assignee-type-select" })] }), selectedTask.assigneeType === "Agent" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Agent profile" }), _jsx(Select, { className: "text-xs", value: selectedTask.assigneeId ??
|
|
1269
|
+
selectedTask.agentProfileId ??
|
|
1270
|
+
"", onValueChange: (value) => updateSelectedTask((currentTask) => ({
|
|
1271
|
+
...currentTask,
|
|
1272
|
+
assigneeType: "Agent",
|
|
1273
|
+
assigneeId: value || null,
|
|
1274
|
+
agentProfileId: value || null,
|
|
1275
|
+
})), options: profileOptions, searchable: true, placeholder: "Select an agent profile", "data-testid": "project-template-task-agent-profile-select" }), profilesError && (_jsx("div", { className: "text-xs text-amber-700", children: profilesError }))] })) : selectedTask.assigneeType === "Human" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Default user identifier" }), _jsx(Input, { value: selectedTask.assigneeId ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
|
|
1276
|
+
...currentTask,
|
|
1277
|
+
assigneeType: "Human",
|
|
1278
|
+
assigneeId: event.target.value,
|
|
1279
|
+
agentProfileId: null,
|
|
1280
|
+
})), placeholder: "sitecore\\\\john", className: "text-xs md:text-xs", "data-testid": "project-template-task-user-assignee-input" })] })) : selectedTask.assigneeType === "Role" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Default role identifier" }), _jsx(Input, { value: selectedTask.assigneeId ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
|
|
1281
|
+
...currentTask,
|
|
1282
|
+
assigneeType: "Role",
|
|
1283
|
+
assigneeId: event.target.value,
|
|
1284
|
+
agentProfileId: null,
|
|
1285
|
+
})), placeholder: "sitecore\\\\Author", className: "text-xs md:text-xs", "data-testid": "project-template-task-role-assignee-input" })] })) : null] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "rounded-lg border border-gray-200 bg-gray-50 p-3", "data-testid": "project-template-dependencies-section", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-800", children: [_jsx(Shield, { className: "h-4 w-4 text-gray-500", strokeWidth: 1.5 }), "Dependencies"] }), _jsxs(Popover, { open: dependencyPickerOpen, onOpenChange: setDependencyPickerOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { type: "button", size: "sm", variant: "outline", className: "h-7 gap-1 px-2", disabled: dependencyAddCandidates.length === 0, "aria-label": "Add dependency", "data-testid": "project-template-add-dependency-button", children: _jsx(Plus, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-72 p-0", sideOffset: 6, "data-testid": "project-template-dependency-popover", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-700", children: "Depends on" }), _jsx("div", { className: "max-h-56 overflow-y-auto p-1", children: dependencyAddCandidates.length ===
|
|
1286
|
+
0 ? (_jsx("div", { className: "px-2 py-3 text-center text-xs text-gray-500", children: "All other tasks are already linked." })) : (dependencyAddCandidates.map((task) => (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-gray-100", onClick: () => handleAddDependency(task.id), "data-testid": `project-template-dependency-option-${task.id}`, children: [_jsx(Link2, { className: "h-3.5 w-3.5 shrink-0 text-gray-400", strokeWidth: 1.5 }), _jsx("span", { className: "min-w-0 truncate font-medium text-gray-900", children: task.title ||
|
|
1287
|
+
"Untitled Task" })] }, task.id)))) })] })] })] }), _jsx("p", { className: "mb-2 text-[11px] leading-snug text-gray-500", children: "This task must wait for these tasks to finish first." }), dependencyTasks.length === 0 ? (_jsx("div", { className: "text-xs text-gray-500", children: draftTemplate.taskTemplates.filter((t) => t.id !== selectedTask.id).length === 0
|
|
1288
|
+
? "Add more task templates to create dependencies."
|
|
1289
|
+
: "No dependencies yet. Use + to add one." })) : (_jsx("ul", { className: "space-y-1.5", children: dependencyTasks.map((task) => (_jsxs("li", { className: "flex items-center justify-between gap-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs", "data-testid": `project-template-dependency-row-${task.id}`, children: [_jsx("span", { className: "min-w-0 truncate font-medium text-gray-800", children: task.title || "Untitled Task" }), _jsx("button", { type: "button", className: "shrink-0 rounded p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-700", "aria-label": `Remove dependency on ${task.title}`, onClick: () => handleRemoveDependency(task.id), "data-testid": `project-template-remove-dependency-${task.id}`, children: _jsx(X, { className: "h-4 w-4", strokeWidth: 1.5 }) })] }, task.id))) }))] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-gray-50 p-3", "data-testid": "project-template-child-tasks-section", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-800", children: [_jsx(CornerDownRight, { className: "h-4 w-4 text-gray-500", strokeWidth: 1.5 }), "Child tasks"] }), _jsx(Button, { type: "button", size: "sm", variant: "outline", className: "h-7 gap-1 px-2", onClick: () => void handleOpenAddChildTaskDialog(), disabled: creatingChildTask, "aria-label": "Add child task", "data-testid": "project-template-add-child-task-button", children: _jsx(Plus, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) })] }), _jsx("p", { className: "mb-2 text-[11px] leading-snug text-gray-500", children: "Tasks that run after this one (they depend on this task)." }), childTaskTemplates.length === 0 ? (_jsx("div", { className: "text-xs text-gray-500", children: "No child tasks yet. Use + to add one." })) : (_jsx("ul", { className: "space-y-1.5", children: childTaskTemplates.map((task) => (_jsxs("li", { className: "flex items-center justify-between gap-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs", "data-testid": `project-template-child-task-row-${task.id}`, children: [_jsx("span", { className: "min-w-0 truncate font-medium text-gray-800", children: task.title || "Untitled Task" }), _jsx("button", { type: "button", className: "shrink-0 rounded p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-700", "aria-label": `Unlink child task ${task.title}`, onClick: () => handleUnlinkChildTask(task.id), "data-testid": `project-template-unlink-child-task-${task.id}`, children: _jsx(X, { className: "h-4 w-4", strokeWidth: 1.5 }) })] }, task.id))) }))] })] })] }) })] })) })),
|
|
1290
|
+
},
|
|
1291
|
+
] }) })] })] })] })) : (_jsx("div", { className: "flex h-full items-center justify-center bg-gray-50/40", children: _jsxs("div", { className: "max-w-sm rounded-lg border border-dashed border-gray-300 bg-white p-8 text-center", children: [_jsx(GitBranch, { className: "mx-auto mb-3 h-10 w-10 text-gray-300", strokeWidth: 1.2 }), _jsx("h3", { className: "text-xs font-semibold text-gray-900", children: "Select a project template" }), _jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Choose a template from the list or create a new one to start editing." })] }) }));
|
|
1292
|
+
const agentPanelContent = (_jsx(ProjectTemplateAgentPanel, { templateName: draftTemplate?.name ?? null, agentId: templateAgentId, loading: templateAgentLoading, resetting: templateAgentResetting, error: templateAgentError, onStartOver: () => void handleResetTemplateAgent() }));
|
|
1293
|
+
const panels = [
|
|
1294
|
+
{
|
|
1295
|
+
name: "project-template-list",
|
|
1296
|
+
defaultSize: 360,
|
|
1297
|
+
content: listContent,
|
|
1298
|
+
className: "overflow-hidden",
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
name: "project-template-detail",
|
|
1302
|
+
defaultSize: "auto",
|
|
1303
|
+
content: detailContent,
|
|
1304
|
+
className: "overflow-hidden",
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
name: "project-template-agent",
|
|
1308
|
+
defaultSize: 460,
|
|
1309
|
+
content: agentPanelContent,
|
|
1310
|
+
className: "overflow-hidden",
|
|
1311
|
+
collapsible: true,
|
|
1312
|
+
},
|
|
1313
|
+
];
|
|
1314
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-full", "data-testid": "project-template-editor", onKeyDownCapture: handleProjectTemplateEditorKeyDownCapture, children: _jsx(Splitter, { panels: panels, localStorageKey: "settings-project-templates-panel-splitter-v2", direction: "horizontal", className: "h-full" }) }), _jsx(Dialog, { open: isCreateTaskDialogOpen, onOpenChange: (open) => {
|
|
1315
|
+
setIsCreateTaskDialogOpen(open);
|
|
1316
|
+
if (!open) {
|
|
1317
|
+
setNewTaskDependencySourceId(null);
|
|
1318
|
+
}
|
|
1319
|
+
}, children: _jsxs(DialogContent, { className: "max-w-md", "data-testid": "project-template-create-task-dialog", children: [_jsx(StyledDialogTitle, { icon: _jsx(Plus, { strokeWidth: 1.5 }), title: newTaskDependencySourceId
|
|
1320
|
+
? "Add Dependent Task"
|
|
1321
|
+
: "Add Task Template", subtitle: newTaskDependencySourceId
|
|
1322
|
+
? "Creates a new task that depends on the selected task."
|
|
1323
|
+
: "Create the task and save it immediately." }), _jsxs("div", { className: "grid gap-2 p-6", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-new-task-title", children: "Task title" }), _jsx(Input, { id: "project-template-new-task-title", value: newTaskTitle, onChange: (event) => setNewTaskTitle(event.target.value), placeholder: "e.g. Discover Sitecore Setup", className: "text-xs md:text-xs", autoFocus: true, onKeyDown: (event) => {
|
|
1324
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1325
|
+
event.preventDefault();
|
|
1326
|
+
void handleConfirmCreateTask();
|
|
1327
|
+
}
|
|
1328
|
+
} })] }), _jsxs(DialogFooter, { className: "px-6 pt-0 pb-6", children: [_jsx(Button, { variant: "outline", onClick: () => setIsCreateTaskDialogOpen(false), disabled: creatingTask, "data-testid": "project-template-create-task-cancel-button", children: "Cancel" }), _jsx(Button, { onClick: () => void handleConfirmCreateTask(), disabled: creatingTask || !newTaskTitle.trim(), "data-testid": "project-template-create-task-submit-button", children: creatingTask
|
|
1329
|
+
? "Creating..."
|
|
1330
|
+
: newTaskDependencySourceId
|
|
1331
|
+
? "Create Dependent Task"
|
|
1332
|
+
: "Create Task" })] })] }) }), _jsx(Dialog, { open: isAddChildTaskDialogOpen, onOpenChange: setIsAddChildTaskDialogOpen, children: _jsxs(DialogContent, { className: "max-w-md", "data-testid": "project-template-create-child-task-dialog", children: [_jsx(StyledDialogTitle, { icon: _jsx(Plus, { strokeWidth: 1.5 }), title: "Add Child Task", subtitle: "Creates a new task that depends on the current task." }), _jsxs("div", { className: "grid gap-2 p-6", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-new-child-task-title", children: "Task title" }), _jsx(Input, { id: "project-template-new-child-task-title", value: newChildTaskTitle, onChange: (event) => setNewChildTaskTitle(event.target.value), placeholder: "e.g. Review content", className: "text-xs md:text-xs", autoFocus: true, onKeyDown: (event) => {
|
|
1333
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
void handleConfirmCreateChildTask();
|
|
1336
|
+
}
|
|
1337
|
+
} })] }), _jsxs(DialogFooter, { className: "px-6 pt-0 pb-6", children: [_jsx(Button, { variant: "outline", onClick: () => setIsAddChildTaskDialogOpen(false), disabled: creatingChildTask, "data-testid": "project-template-create-child-task-cancel-button", children: "Cancel" }), _jsx(Button, { onClick: () => void handleConfirmCreateChildTask(), disabled: creatingChildTask || !newChildTaskTitle.trim(), "data-testid": "project-template-create-child-task-submit-button", children: creatingChildTask ? "Creating..." : "Create Task" })] })] }) })] }));
|
|
1338
|
+
}
|
|
1339
|
+
export default ProjectTemplatesPanel;
|
|
1340
|
+
//# sourceMappingURL=ProjectTemplatesPanel.js.map
|