gui: sidebar tree UX fixes — has_children, preserve expanded, double-click, DnD visual

- Backend: add HasChildren field to NodeDTO; ListWorkspaceTree/ListChildren
  populate it by querying CountChildren for container types
- Node repository: add CountChildren(parentID, types...) method
- TreeNode: toggle shown only when has_children (not isContainer); double-click
  on row = expand/collapse; icon click = expand/collapse; drop-valid class via
  DOM classList for DnD highlight; auto-expand collapsed container on 500ms
  hover during drag; auto-scroll near edge during drag
- App.svelte: selectNode no longer resets workspaceTree/expanded; new
  reloadTreePreservingExpanded() helper re-fetches children for all expanded
  nodes after DnD move / delete; deleteWorkspaceNode preserves expanded state
This commit is contained in:
2026-06-03 03:48:53 +08:00
parent 9260582072
commit c941f05dab
10 changed files with 111 additions and 65 deletions
+11 -10
View File
@@ -104,16 +104,17 @@ func (a *App) autoSyncLoop() {
// ============================================================
type NodeDTO struct {
ID string `json:"id"`
ParentID *string `json:"parent_id,omitempty"`
Type string `json:"type"`
Title string `json:"title"`
TemplateID string `json:"template_id"`
FsPath string `json:"fs_path"`
SortOrder int `json:"sort_order"`
Archived bool `json:"archived"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID string `json:"id"`
ParentID *string `json:"parent_id,omitempty"`
Type string `json:"type"`
Title string `json:"title"`
TemplateID string `json:"template_id"`
FsPath string `json:"fs_path"`
SortOrder int `json:"sort_order"`
Archived bool `json:"archived"`
HasChildren bool `json:"has_children"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type TemplateDTO struct {
+18 -2
View File
@@ -19,7 +19,15 @@ func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
if err != nil {
return nil, err
}
return filterContainers(toNodeDTOs(list)), nil
dtos := filterContainers(toNodeDTOs(list))
for i := range dtos {
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
if err != nil {
return nil, err
}
dtos[i].HasChildren = n > 0
}
return dtos, nil
}
func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) {
@@ -27,7 +35,15 @@ func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) {
if err != nil {
return nil, err
}
return filterContainers(toNodeDTOs(list)), nil
dtos := filterContainers(toNodeDTOs(list))
for i := range dtos {
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
if err != nil {
return nil, err
}
dtos[i].HasChildren = n > 0
}
return dtos, nil
}
func filterContainers(dtos []NodeDTO) []NodeDTO {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-Dt9BLCiY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CHi9sCXv.css">
<script type="module" crossorigin src="/assets/main-y7k2Kn7r.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CJu7OoK7.css">
</head>
<body>
<div id="app"></div>