gui: sidebar tree model fix — only container nodes, improved DnD + context menu

- Backend: ListWorkspaceTree/ListWorkspaceChildren filter to container types
  only (case, client, project, folder, document, recipe)
- TreeNode: full-row context menu (removed label stopPropagation),
  double-click toggles expand, icon-click toggles expand, DnD auto-expand
  on 500ms hover, auto-scroll near edges, drag-over highlight via classList
- App.svelte: toggleExpand uses ListWorkspaceChildren, submitCreateNode uses
  ListWorkspaceChildren for child tree population
- Note/file nodes no longer appear in the sidebar workspace tree
This commit is contained in:
mirivlad 2026-06-03 03:33:13 +08:00
parent b2dcb116c9
commit 9260582072
8 changed files with 104 additions and 20 deletions

View File

@ -19,7 +19,26 @@ func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
return filterContainers(toNodeDTOs(list)), nil
}
func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) {
list, err := a.nodes.ListChildren(parentID, false)
if err != nil {
return nil, err
}
return filterContainers(toNodeDTOs(list)), nil
}
func filterContainers(dtos []NodeDTO) []NodeDTO {
out := make([]NodeDTO, 0, len(dtos))
for _, d := range dtos {
switch d.Type {
case "case", "client", "project", "folder", "document", "recipe":
out = append(out, d)
}
}
return out
}
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {

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

View File

@ -16,8 +16,8 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-CBFn_7kC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-C49DuBq6.css">
<script type="module" crossorigin src="/assets/main-Dt9BLCiY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CHi9sCXv.css">
</head>
<body>
<div id="app"></div>

View File

@ -643,7 +643,7 @@
createWithTemplate = undefined
if (parentID) {
expanded = { ...expanded, [parentID]: true }
const children = await wailsCall('ListChildren', parentID) || []
const children = await wailsCall('ListWorkspaceChildren', parentID) || []
setNodeChildren(workspaceTree, parentID, children)
workspaceTree = [...workspaceTree]
} else {
@ -673,7 +673,7 @@
const willExpand = !expanded[nodeId]
expanded = { ...expanded, [nodeId]: willExpand }
if (!willExpand) return
const children = await wailsCall('ListChildren', nodeId) || []
const children = await wailsCall('ListWorkspaceChildren', nodeId) || []
setNodeChildren(workspaceTree, nodeId, children)
workspaceTree = [...workspaceTree]
}

View File

@ -10,6 +10,9 @@
export let onContextMenu
export let onDrop
let autoExpandTimers = {}
let scrollInterval = null
function iconKind(node) {
if (node.type === 'client' || node.template_id === 'client.default') return 'client'
if (node.type === 'project' || node.template_id === 'project.default') return 'project'
@ -63,15 +66,69 @@
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
e.currentTarget.classList.add('drag-over')
if (isContainer(node) && !expanded[node.id] && !autoExpandTimers[node.id]) {
autoExpandTimers[node.id] = setTimeout(() => {
if (onToggle) onToggle(node.id)
delete autoExpandTimers[node.id]
}, 500)
}
const rect = e.currentTarget.closest('.workspace-tree-area') || document.querySelector('.workspace-tree-area')
if (rect) {
const areaRect = rect.getBoundingClientRect()
const threshold = 30
if (e.clientY - areaRect.top < threshold) {
if (!scrollInterval) {
scrollInterval = setInterval(() => { rect.scrollTop -= 10 }, 50)
}
} else if (areaRect.bottom - e.clientY < threshold) {
if (!scrollInterval) {
scrollInterval = setInterval(() => { rect.scrollTop += 10 }, 50)
}
} else {
if (scrollInterval) { clearInterval(scrollInterval); scrollInterval = null }
}
}
}
function handleDragLeave(e, node) {
e.currentTarget.classList.remove('drag-over')
if (autoExpandTimers[node.id]) {
clearTimeout(autoExpandTimers[node.id])
delete autoExpandTimers[node.id]
}
if (scrollInterval) { clearInterval(scrollInterval); scrollInterval = null }
}
function handleDrop(e, node) {
e.preventDefault()
e.stopPropagation()
e.currentTarget.classList.remove('drag-over')
if (autoExpandTimers[node.id]) {
clearTimeout(autoExpandTimers[node.id])
delete autoExpandTimers[node.id]
}
if (scrollInterval) { clearInterval(scrollInterval); scrollInterval = null }
const draggedId = getDraggedId(e)
if (!canDrop(node, draggedId)) return
if (onDrop) onDrop(draggedId, node.id)
}
function handleRowClick(e, node) {
const toggleEl = e.target.closest('.tree-toggle')
const iconEl = e.target.closest('.tree-icon')
if (toggleEl || iconEl) return
if (onSelect) onSelect(node)
}
function handleRowDblClick(e, node) {
if (isContainer(node) && onToggle) onToggle(node.id)
}
function handleIconClick(e, node) {
e.stopPropagation()
if (isContainer(node) && onToggle) onToggle(node.id)
}
</script>
{#each nodes as node}
@ -81,23 +138,22 @@
draggable="true"
on:dragstart={(e) => handleDragStart(e, node)}
on:dragover={(e) => handleDragOver(e, node)}
on:dragleave={(e) => handleDragLeave(e, node)}
on:drop={(e) => handleDrop(e, node)}
on:click={(e) => handleRowClick(e, node)}
on:dblclick={(e) => handleRowDblClick(e, node)}
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
{#if isContainer(node)}
<button class="tree-toggle" on:click|stopPropagation={() => onToggle && onToggle(node.id)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{#if expanded[node.id]}
<span class="tree-arrow"></span>
{:else}
<span class="tree-arrow"></span>
{/if}
<button class="tree-toggle" on:click|stopPropagation={() => onToggle && onToggle(node.id)}>
<span class="tree-arrow">{expanded[node.id] ? '▾' : '▸'}</span>
</button>
{:else}
<span class="tree-toggle-placeholder"></span>
{/if}
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
<span class="tree-label" on:click|stopPropagation={() => onSelect && onSelect(node)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
<span class="tree-icon" on:click={(e) => handleIconClick(e, node)} on:dblclick|stopPropagation>
<TemplateIcon kind={iconKind(node)} size={16} />
</span>
<span class="tree-label" on:click|stopPropagation={() => onSelect && onSelect(node)}>
{node.title}
</span>
</div>
@ -126,6 +182,11 @@
color: #fff;
font-weight: 500;
}
.tree-item.drag-over {
background: #1a3a1a;
outline: 1px solid #4ade80;
outline-offset: -1px;
}
.tree-toggle {
background: none;
border: none;
@ -143,7 +204,7 @@
font-size: 12px;
}
.tree-toggle:hover {
color: #888;
color: #a5b4fc;
}
.tree-toggle-placeholder {
display: inline-block;
@ -159,6 +220,10 @@
flex-shrink: 0;
color: #888;
margin-right: 4px;
cursor: pointer;
}
.tree-icon:hover {
color: #a5b4fc;
}
.tree-item.selected .tree-icon {
color: #a5b4fc;