fix(canvas): make collapse reachable + persist for every node type

Three connected bugs in PR #158's collapse feature:

1. Toggle wired to the wrong component
   The chevron was on GroupRectNode and computed children via React Flow
   parentId. But in this codebase parentId is set by createGroup() on
   type=group nodes, not on groupRect zones — zones are decorative
   rectangles. Result: childrenCount was always 0 on every zone and the
   button never rendered, so the feature was unreachable from the UI.

   Fix:
   - Add the same chevron toggle to GroupNode (the actual container).
     parentId children are already known there, so the existing BFS in
     computeCollapseInfo hides them when data.collapsed flips.
   - For GroupRectNode, switch childrenCount to spatial containment so
     drawn zones also work: hit-test other nodes' bbox centres against
     the zone bbox.

2. Visibility filter ignored spatial zones
   Extend computeCollapseInfo with a second pass that hides every node
   whose centre lies inside a collapsed groupRect, plus the parentId
   subtrees of those nodes (so a Proxmox host inside a collapsed zone
   takes its VMs/LXCs with it). Edge rewiring routes vanished endpoints
   to the same visible zone via a unified hiddenBy map populated by both
   passes.

3. Save dropped data.collapsed for every type except groupRect
   The DevTools payload was the smoking gun: for a type=group node the
   serializer wrote custom_colors: {show_border: true} with no collapsed
   key, so the backend stored a stale false on every save. Only the
   groupRect branch of serializeNode/deserializeApiNode stashed and
   hoisted the flag. Move the stash + hoist to the general branch too
   (backend's custom_colors is dict[str, Any] so no schema change).

Tests: 11 new cases for spatial containment + GroupNode toggle UI, and
4 round-trip cases for collapse on non-groupRect types.
This commit is contained in:
Pouzor
2026-05-29 08:58:36 +02:00
parent 20e1820a4e
commit b71c96897a
8 changed files with 486 additions and 145 deletions
+15 -2
View File
@@ -97,7 +97,13 @@ export function serializeNode(n: Node<NodeData>): Record<string, unknown> {
notes: n.data.notes ?? null,
parent_id: n.data.parent_id ?? null,
container_mode: n.data.container_mode ?? false,
custom_colors: n.data.custom_colors ?? null,
// Stash collapse state inside the custom_colors blob so the backend's
// dict[str, Any] column carries it without a schema change. Hoisted
// back to `data.collapsed` on load. Applies to every node type — group
// containers, Proxmox hosts, etc. — not just groupRect zones.
custom_colors: n.data.collapsed !== undefined
? { ...(n.data.custom_colors ?? {}), collapsed: n.data.collapsed }
: (n.data.custom_colors ?? null),
custom_icon: n.data.custom_icon ?? null,
cpu_count: n.data.cpu_count ?? null,
cpu_model: n.data.cpu_model ?? null,
@@ -162,7 +168,14 @@ export function deserializeApiNode(
id: n.id,
type: normalizedType,
position: { x: n.pos_x, y: n.pos_y },
data: { ...n, type: normalizedType, bottom_handles: clampBottomHandles(n.bottom_handles ?? 1) } as unknown as NodeData,
// Hoist persisted collapse flag from the custom_colors stash (matches
// the symmetric serialize step). Applies to every node type.
data: {
...n,
type: normalizedType,
bottom_handles: clampBottomHandles(n.bottom_handles ?? 1),
collapsed: Boolean(n.custom_colors?.collapsed),
} as unknown as NodeData,
...(n.parent_id && parentIsContainer ? { parentId: n.parent_id, extent: 'parent' as const } : {}),
...(['proxmox', 'vm', 'lxc', 'docker_host'].includes(normalizedType) && n.container_mode !== false
? { width: n.width ?? 300, height: n.height ?? 200 }