From 286dcbfd1e613132d5c20111d15d8a5e34c3b5d8 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 23 Apr 2026 20:36:55 -0700 Subject: [PATCH] fix(canvas,org): collapse org-imported parents on first paint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing a 15-workspace org template dropped every child as a freely-positioned card into its parent's coordinate space. Parents with 5-10 kids had the kids spill below the parent's initial min size, producing the "ugly default" layout the user just flagged — a mess of overlapping cards the moment the import completed. Fix: every workspace in an org-template import that HAS children is inserted with `collapsed = true`. Leaf workspaces stay expanded (nothing to hide). The canvas renders a collapsed parent as a compact header-only card with its "N sub" badge — visually identical to the pre-refactor default the user asked for. Double-click on a collapsed parent now EXPANDS it (flipping `collapsed` locally + persisting via PATCH) so the user can drill in to see the subtree. Only once expanded does a second double-click zoom-to-team, matching the prior behaviour. Leaf-first creation order stays the same; the collapsed flag just means "render compact" not "hide from API". Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/WorkspaceNode.tsx | 18 ++++++++++++++++-- .../internal/handlers/org_import.go | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/canvas/src/components/WorkspaceNode.tsx b/canvas/src/components/WorkspaceNode.tsx index e915e17d..a5e3192f 100644 --- a/canvas/src/components/WorkspaceNode.tsx +++ b/canvas/src/components/WorkspaceNode.tsx @@ -81,9 +81,23 @@ export function WorkspaceNode({ id, data }: NodeProps>) }} onDoubleClick={(e) => { e.stopPropagation(); - if (hasChildren) { - window.dispatchEvent(new CustomEvent("molecule:zoom-to-team", { detail: { nodeId: id } })); + if (!hasChildren) return; + // A collapsed parent double-click EXPANDS first (flipping the + // collapsed flag + persisting it via the API). Once expanded, + // subsequent double-clicks zoom-to-team so the user can see + // the hierarchy fit in the viewport. Matches the user's ask: + // default-collapsed for clean first paint, one gesture reveals + // the subtree. + if (data.collapsed) { + const state = useCanvasStore.getState(); + state.setCollapsed(id, false); + // Fire-and-forget persist so reload retains the expansion. + import("@/lib/api").then(({ api }) => { + api.patch(`/workspaces/${id}`, { collapsed: false }).catch(() => {}); + }); + return; } + window.dispatchEvent(new CustomEvent("molecule:zoom-to-team", { detail: { nodeId: id } })); }} onContextMenu={(e) => { e.preventDefault(); diff --git a/workspace-server/internal/handlers/org_import.go b/workspace-server/internal/handlers/org_import.go index 8435f8a0..bfc3e251 100644 --- a/workspace-server/internal/handlers/org_import.go +++ b/workspace-server/internal/handlers/org_import.go @@ -88,11 +88,19 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa ctx := context.Background() - // Insert workspace + // Org-template imports can drop dozens of nested workspaces onto the + // canvas at once. Letting them render expanded by default sprays + // child cards across the viewport (sibling workspaces spill below + // the parent before the user can orient themselves). Default every + // parent in the imported tree to collapsed — the parent card shows + // only its header + "N sub" badge until the user double-clicks to + // expand it. Leaf workspaces stay expanded (nothing to hide). + initialCollapsed := len(ws.Children) > 0 + _, err := db.DB.ExecContext(ctx, ` - INSERT INTO workspaces (id, name, role, tier, runtime, awareness_namespace, status, parent_id, workspace_dir, workspace_access) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - `, id, ws.Name, role, tier, runtime, awarenessNS, "provisioning", parentID, workspaceDir, workspaceAccess) + INSERT INTO workspaces (id, name, role, tier, runtime, awareness_namespace, status, parent_id, workspace_dir, workspace_access, collapsed) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + `, id, ws.Name, role, tier, runtime, awarenessNS, "provisioning", parentID, workspaceDir, workspaceAccess, initialCollapsed) if err != nil { log.Printf("Org import: failed to create %s: %v", ws.Name, err) return fmt.Errorf("failed to create %s: %w", ws.Name, err)