fix(canvas,org): collapse org-imported parents on first paint

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) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-23 20:36:55 -07:00
parent 507696d88a
commit 286dcbfd1e
2 changed files with 28 additions and 6 deletions

View File

@ -81,9 +81,23 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
}}
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();

View File

@ -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)