From 03b56fa5af97a45c3d1b88a5bab20c470e680835 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 23 Apr 2026 16:24:49 -0700 Subject: [PATCH] fix(canvas): collapse Org Templates section by default in palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TemplatePalette's Org Templates section rendered all cards inline, each ~120 px tall (name + description + "Import org" button). With 4 org templates on disk that's ~500 px of drawer height — the individual workspace templates at the top (AutoGen / LangGraph / Hermes / …) got pushed off-screen, which is the exact complaint from the test session ("templates still 90% org, cant even see normal workspace template"). Collapsed the Org Templates section by default. The header now toggles with an ▶ caret and shows the count ("Org Templates (4)"). Clicking expands to reveal the full card list; clicking again collapses. Persists only within a session — fresh mounts start collapsed so the primary deploy path stays visible. Individual workspace templates are the usual starting point (pick a runtime, deploy one agent), while org templates are a heavier "deploy this whole pre-built team" action. Making the second expandable matches the relative frequency. - `TemplatePalette.tsx::OrgTemplatesSection` — added `expanded` state (default false), wrapped the cards in `{expanded && …}`, turned the header into a toggle button with `aria-expanded` + `aria-controls`. - `__tests__/OrgTemplatesSection.test.tsx` — 3 new rendering tests: collapsed-by-default (cards absent), click expands (cards appear), click again collapses (cards gone). Mocks /org/templates with a 2-entry response so the count assertion is stable. Full canvas vitest: 930/930 pass (up from 927). Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/TemplatePalette.tsx | 32 +++++- .../__tests__/OrgTemplatesSection.test.tsx | 102 ++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 canvas/src/components/__tests__/OrgTemplatesSection.test.tsx diff --git a/canvas/src/components/TemplatePalette.tsx b/canvas/src/components/TemplatePalette.tsx index 2d2b1718..79fd42ae 100644 --- a/canvas/src/components/TemplatePalette.tsx +++ b/canvas/src/components/TemplatePalette.tsx @@ -54,6 +54,13 @@ export function OrgTemplatesSection() { const [loading, setLoading] = useState(false); const [importing, setImporting] = useState(null); const [error, setError] = useState(null); + // Collapsed by default — org templates are multi-workspace imports + // that most new users don't reach for first. Keeping them + // expand-on-demand frees ~400 px of vertical space for the + // individual workspace templates above, which is the primary + // deploy path. The count in the header still makes discovery + // obvious: "Org Templates (4) ▸". + const [expanded, setExpanded] = useState(false); const loadOrgs = useCallback(async () => { setLoading(true); @@ -80,9 +87,26 @@ export function OrgTemplatesSection() { return (
-

+

+ {orgs.length > 0 && ( + + ({orgs.length}) + + )} +
+ {expanded && ( +
{loading && (
@@ -141,6 +167,8 @@ export function OrgTemplatesSection() {
); })} +
+ )}
); } diff --git a/canvas/src/components/__tests__/OrgTemplatesSection.test.tsx b/canvas/src/components/__tests__/OrgTemplatesSection.test.tsx new file mode 100644 index 00000000..59bdda12 --- /dev/null +++ b/canvas/src/components/__tests__/OrgTemplatesSection.test.tsx @@ -0,0 +1,102 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, waitFor, fireEvent, cleanup } from "@testing-library/react"; + +// Tests for the default-collapsed + expand-on-click behavior of the +// org templates drawer. Before this change the section rendered all +// org cards inline, which pushed the individual workspace templates +// off-screen when there were ≥3 orgs on disk. Collapsed-by-default +// keeps the scroll focused on the primary deploy path. + +vi.mock("@/lib/api", () => ({ + api: { + get: vi.fn().mockResolvedValue([ + { dir: "free-beats-all", name: "Free Beats All", description: "d1", workspaces: 3 }, + { dir: "medo-smoke", name: "MeDo Smoke Test", description: "d2", workspaces: 1 }, + ]), + post: vi.fn().mockResolvedValue({}), + }, +})); + +vi.mock("../Spinner", () => ({ Spinner: () => null })); +vi.mock("../MissingKeysModal", () => ({ MissingKeysModal: () => null })); +vi.mock("../ConfirmDialog", () => ({ ConfirmDialog: () => null })); +vi.mock("@/lib/deploy-preflight", () => ({ checkDeploySecrets: vi.fn() })); + +import { OrgTemplatesSection } from "../TemplatePalette"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + cleanup(); +}); + +describe("OrgTemplatesSection — collapse/expand", () => { + it("renders collapsed by default — org cards are NOT in the DOM", async () => { + render(); + // The header toggle is visible immediately… + // Two buttons match "Org Templates" (toggle + refresh) — pick the + // toggle by its aria-controls binding. + const toggle = (await screen.findAllByRole("button")).find((b) => + b.getAttribute("aria-controls") === "org-templates-body" + )!; + expect(toggle).toBeTruthy(); + expect(toggle.getAttribute("aria-expanded")).toBe("false"); + + // …and the count appears after loadOrgs resolves. + await waitFor(() => { + expect(toggle.textContent).toContain("(2)"); + }); + + // But none of the individual org cards should be rendered yet. + expect(screen.queryByText("Free Beats All")).toBeNull(); + expect(screen.queryByText("MeDo Smoke Test")).toBeNull(); + }); + + it("clicking the header reveals the org cards", async () => { + render(); + + // Wait for the count so we know loadOrgs finished. + // Two buttons match "Org Templates" (toggle + refresh) — pick the + // toggle by its aria-controls binding. + const toggle = (await screen.findAllByRole("button")).find((b) => + b.getAttribute("aria-controls") === "org-templates-body" + )!; + await waitFor(() => { + expect(toggle.textContent).toContain("(2)"); + }); + + // Expand. + fireEvent.click(toggle); + await waitFor(() => { + expect(toggle.getAttribute("aria-expanded")).toBe("true"); + }); + + // Org cards now visible. + expect(screen.getByText("Free Beats All")).toBeTruthy(); + expect(screen.getByText("MeDo Smoke Test")).toBeTruthy(); + }); + + it("clicking the header again collapses back", async () => { + render(); + // Two buttons match "Org Templates" (toggle + refresh) — pick the + // toggle by its aria-controls binding. + const toggle = (await screen.findAllByRole("button")).find((b) => + b.getAttribute("aria-controls") === "org-templates-body" + )!; + await waitFor(() => { + expect(toggle.textContent).toContain("(2)"); + }); + + fireEvent.click(toggle); // expand + expect(screen.getByText("Free Beats All")).toBeTruthy(); + + fireEvent.click(toggle); // collapse + await waitFor(() => { + expect(toggle.getAttribute("aria-expanded")).toBe("false"); + }); + expect(screen.queryByText("Free Beats All")).toBeNull(); + }); +});