From 2284d6683e26187f21b97656ce99648ab8020f68 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 30 Jun 2026 17:15:59 -0700 Subject: [PATCH] FN-7319: Collapse task-detail branch groups Make shared-branch task details open in a compact collapsed state by default. - Default branch group cards to collapsed and remount them when switching tasks in the same group. - Add compact token-based styling for collapsed branch group summaries. - Update branch group and task detail tests for collapsed defaults and state reset behavior. - Add a patch changeset for the published CLI bundle. Files changed: .changeset/fn-7319-compact-branch-groups.md | 7 ++++ .../dashboard/app/components/BranchGroupCard.css | 38 +++++++++++++++++- .../dashboard/app/components/BranchGroupCard.tsx | 30 +++----------- .../dashboard/app/components/TaskDetailModal.tsx | 3 +- .../components/__tests__/BranchGroupCard.test.tsx | 46 ++++++++++++++++++---- .../components/__tests__/TaskDetailModal.test.tsx | 38 +++++++++++++++--- 6 files changed, 123 insertions(+), 39 deletions(-) Fusion-Task-Id: FN-7319 Fusion-Task-Lineage: a4469ecd-cffa-4af4-b4dc-064246ff9a39 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7319-compact-branch-groups.md | 7 +++ .../app/components/BranchGroupCard.css | 38 ++++++++++++++- .../app/components/BranchGroupCard.tsx | 30 +++--------- .../app/components/TaskDetailModal.tsx | 3 +- .../__tests__/BranchGroupCard.test.tsx | 46 +++++++++++++++---- .../__tests__/TaskDetailModal.test.tsx | 38 +++++++++++++-- 6 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 .changeset/fn-7319-compact-branch-groups.md diff --git a/.changeset/fn-7319-compact-branch-groups.md b/.changeset/fn-7319-compact-branch-groups.md new file mode 100644 index 0000000000..28f1417428 --- /dev/null +++ b/.changeset/fn-7319-compact-branch-groups.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Collapse task-detail branch groups by default with a more compact summary. +category: fix +dev: Keeps branch-group member and PR actions available after expanding the task-detail card. diff --git a/packages/dashboard/app/components/BranchGroupCard.css b/packages/dashboard/app/components/BranchGroupCard.css index 86278c28c9..3a8caf3bc2 100644 --- a/packages/dashboard/app/components/BranchGroupCard.css +++ b/packages/dashboard/app/components/BranchGroupCard.css @@ -4,6 +4,15 @@ gap: var(--space-sm); } +/* +FNXC:BranchGroupDetails 2026-06-30-00:00: +Collapsed branch-group details must consume less task-detail space with token-based typography and spacing while keeping the expand button large enough to remain usable. +*/ +.branch-group-card--collapsed { + gap: calc(var(--space-xs) / 2); + padding: var(--space-sm); +} + .branch-group-card-error { color: var(--color-error); } @@ -21,12 +30,22 @@ gap: var(--space-xs); } +.branch-group-card--collapsed .branch-group-card-title { + font-size: var(--font-size-xs); + line-height: var(--line-height-tight); +} + .branch-group-card-header-meta { display: inline-flex; align-items: center; gap: var(--space-xs); } +.branch-group-card--collapsed .branch-group-card-header, +.branch-group-card--collapsed .branch-group-card-header-meta { + gap: calc(var(--space-xs) / 2); +} + .branch-group-card-badge { background: var(--surface-1); } @@ -39,13 +58,22 @@ padding: calc(var(--space-xs) / 2); } +.branch-group-card--collapsed .branch-group-card-header-meta .btn-icon { + padding: 0; +} + .branch-group-card-header-meta .btn-icon svg { display: block; } .branch-group-card-progress-text { color: var(--text-muted); - font-size: 0.875rem; + font-size: calc(var(--font-size-base) * 0.875); +} + +.branch-group-card--collapsed .branch-group-card-progress-text { + font-size: var(--font-size-xs); + line-height: var(--line-height-tight); } .branch-group-card-progress { @@ -56,6 +84,10 @@ overflow: hidden; } +.branch-group-card--collapsed .branch-group-card-progress { + height: calc(var(--space-xs) / 2); +} + .branch-group-card-progress-fill { display: block; height: 100%; @@ -103,4 +135,8 @@ width: 100%; justify-content: space-between; } + + .branch-group-card--collapsed .branch-group-card-header { + gap: calc(var(--space-xs) / 2); + } } diff --git a/packages/dashboard/app/components/BranchGroupCard.tsx b/packages/dashboard/app/components/BranchGroupCard.tsx index 7f7c4e9950..fd86065ce3 100644 --- a/packages/dashboard/app/components/BranchGroupCard.tsx +++ b/packages/dashboard/app/components/BranchGroupCard.tsx @@ -18,7 +18,11 @@ export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) { const [error, setError] = useState(null); const [promoting, setPromoting] = useState(false); const [abandoning, setAbandoning] = useState(false); - const [collapsed, setCollapsed] = useState(false); + /* + FNXC:BranchGroupDetails 2026-06-30-00:00: + Task-detail branch groups must be collapsed by default on every breakpoint while preserving the user's expand/collapse control for member and action inspection. + */ + const [collapsed, setCollapsed] = useState(true); const loadGroup = useCallback(async () => { try { @@ -38,28 +42,6 @@ export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) { void loadGroup(); }, [loadGroup]); - useEffect(() => { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") { - return; - } - const mediaQuery = window.matchMedia("(max-width: 768px)"); - const syncCollapsed = (matches: boolean) => { - setCollapsed(matches); - }; - - syncCollapsed(mediaQuery.matches); - const onMediaChange = (event: MediaQueryListEvent) => { - syncCollapsed(event.matches); - }; - - if (typeof mediaQuery.addEventListener === "function") { - mediaQuery.addEventListener("change", onMediaChange); - return () => mediaQuery.removeEventListener("change", onMediaChange); - } - - mediaQuery.addListener(onMediaChange); - return () => mediaQuery.removeListener(onMediaChange); - }, []); useEffect(() => { const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; @@ -117,7 +99,7 @@ export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) { const complete = group.completion.complete; return ( -
+
diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index cecb9811de..a4068c5c08 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -3115,7 +3115,8 @@ export function TaskDetailContent({
{task.branchContext?.groupId && ( - + /* FNXC:BranchGroupDetails 2026-06-30-00:00: Task-detail branch groups must return to their compact collapsed default when users switch tasks, including between members of the same shared branch group. Key by task and group so a manual expansion never leaks into the next task detail view. */ + )} {/* FNXC:Workspace 2026-06-21-00:00: workspace tasks have no singular task.worktree/task.branch; surface their acquired per-sub-repo worktrees diff --git a/packages/dashboard/app/components/__tests__/BranchGroupCard.test.tsx b/packages/dashboard/app/components/__tests__/BranchGroupCard.test.tsx index 86d13e270f..b6038d5430 100644 --- a/packages/dashboard/app/components/__tests__/BranchGroupCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/BranchGroupCard.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { BranchGroupCard } from "../BranchGroupCard"; +import { loadAllAppCssBaseOnly } from "../../test/cssFixture"; const apiGetBranchGroup = vi.fn(); const apiPromoteBranchGroup = vi.fn(); @@ -28,6 +29,13 @@ vi.mock("lucide-react", () => ({ Loader2: () => null, })); +async function expandBranchGroup() { + const toggle = await screen.findByRole("button", { name: /expand branch group/i }); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(toggle); + expect(screen.getByRole("button", { name: /collapse branch group/i })).toHaveAttribute("aria-expanded", "true"); +} + function makeGroup(overrides: Record = {}) { return { id: "BG-1", @@ -59,6 +67,7 @@ describe("BranchGroupCard", () => { apiGetBranchGroup.mockResolvedValue({ group: makeGroup() }); render(); await screen.findByText("1 of 2 members finished"); + await expandBranchGroup(); expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull(); }); @@ -69,6 +78,7 @@ describe("BranchGroupCard", () => { apiPromoteBranchGroup.mockResolvedValue({ groupId: "BG-1", prState: "open" }); render(); + await expandBranchGroup(); const button = await screen.findByRole("button", { name: /open pr/i }); fireEvent.click(button); @@ -87,6 +97,7 @@ describe("BranchGroupCard", () => { }); render(); + await expandBranchGroup(); expect(await screen.findByText("Auto-merge enabled")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull(); }); @@ -96,6 +107,7 @@ describe("BranchGroupCard", () => { group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }], prState: "open", prNumber: 9, prUrl: "https://example/pr/9" }), }); render(); + await expandBranchGroup(); expect(await screen.findByRole("link", { name: /pr #9/i })).toBeInTheDocument(); }); @@ -111,6 +123,7 @@ describe("BranchGroupCard", () => { apiAbandonBranchGroup.mockResolvedValue({ groupId: "BG-1", group: makeGroup({ status: "abandoned", prState: "closed" }) }); render(); + await expandBranchGroup(); const abandon = await screen.findByRole("button", { name: /abandon group/i }); fireEvent.click(abandon); @@ -137,6 +150,7 @@ describe("BranchGroupCard", () => { }); render(); + await expandBranchGroup(); expect(await screen.findByRole("button", { name: /abandon group/i })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull(); }); @@ -146,6 +160,7 @@ describe("BranchGroupCard", () => { group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: completeMembers, prState: "merged", prNumber: 5, prUrl: "https://example/pr/5" }), }); render(); + await expandBranchGroup(); expect(await screen.findByText("Group PR merged")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /open pr|merge group into main|abandon group/i })).toBeNull(); }); @@ -155,23 +170,38 @@ describe("BranchGroupCard", () => { group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: completeMembers, prState: "closed", prNumber: 6, prUrl: "https://example/pr/6" }), }); render(); + await expandBranchGroup(); expect(await screen.findByText("Group PR closed")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /open pr|merge group into main|abandon group/i })).toBeNull(); }); - it("shows members by default and collapses via toggle", async () => { - apiGetBranchGroup.mockResolvedValue({ group: makeGroup() }); + it("starts populated groups collapsed and expands via toggle", async () => { + apiGetBranchGroup.mockResolvedValue({ group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: completeMembers }) }); render(); - expect(await screen.findByText("FN-1 · one")).toBeInTheDocument(); + expect(await screen.findByText("2 of 2 members finished")).toBeInTheDocument(); - const toggle = screen.getByRole("button", { name: /collapse branch group/i }); - expect(toggle).toHaveAttribute("aria-expanded", "true"); + const toggle = screen.getByRole("button", { name: /expand branch group/i }); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByText("FN-1 · one")).toBeNull(); + expect(screen.queryByRole("button", { name: /open pr|merge group into main|abandon group/i })).toBeNull(); fireEvent.click(toggle); - expect(screen.getByRole("button", { name: /expand branch group/i })).toHaveAttribute("aria-expanded", "false"); - expect(screen.queryByText("FN-1 · one")).toBeNull(); - expect(screen.getByText("1 of 2 members finished")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /collapse branch group/i })).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByText("FN-1 · one")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /open pr/i })).toBeInTheDocument(); + }); + + it("keeps collapsed branch-group styling tokenized and compact", () => { + const css = loadAllAppCssBaseOnly(); + const collapsedBlock = css.match(/\.branch-group-card--collapsed\s*\{(?[^}]*)\}/)?.groups?.block ?? ""; + expect(collapsedBlock).toContain("gap: calc(var(--space-xs) / 2)"); + expect(collapsedBlock).toContain("padding: var(--space-sm)"); + expect(collapsedBlock).not.toMatch(/\d+px|#[0-9a-f]{3,8}|rgba?\(/i); + + const titleBlock = css.match(/\.branch-group-card--collapsed \.branch-group-card-title\s*\{(?[^}]*)\}/)?.groups?.block ?? ""; + expect(titleBlock).toContain("font-size: var(--font-size-xs)"); + expect(titleBlock).toContain("line-height: var(--line-height-tight)"); }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx index 5970d154c7..091356e1da 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx @@ -21,7 +21,16 @@ import { import { TaskDetailContent, TaskDetailModal } from "../TaskDetailModal"; vi.mock("../BranchGroupCard", () => ({ - BranchGroupCard: ({ groupId }: { groupId: string }) =>
Mock Branch Group {groupId}
, + BranchGroupCard: ({ groupId }: { groupId: string }) => { + const [expanded, setExpanded] = React.useState(false); + return ( +
+ Mock Branch Group {groupId} + + {expanded && Mock branch group expanded} +
+ ); + }, })); setupTaskDetailModalHooks(); @@ -593,20 +602,39 @@ describe("TaskDetailModal Raw Logs agent loading", () => { }); describe("TaskDetailModal branch group surfacing", () => { - it("renders branch group card when task has group context", () => { - render( + const branchContext = { groupId: "BG-1", source: "planning", assignmentMode: "shared" } as const; + + function renderTaskWithBranchContext(id: string) { + return ( , + /> ); + } + it("renders branch group card when task has group context", () => { + render(renderTaskWithBranchContext("FN-6041")); + + expect(screen.getByText("Mock Branch Group BG-1")).toBeInTheDocument(); + }); + + it("remounts the branch group card when switching tasks inside the same group", async () => { + const user = userEvent.setup(); + const { rerender } = render(renderTaskWithBranchContext("FN-6041")); + + await user.click(screen.getByRole("button", { name: "Mock expand branch group" })); + expect(screen.getByText("Mock branch group expanded")).toBeInTheDocument(); + + rerender(renderTaskWithBranchContext("FN-6042")); + + expect(screen.queryByText("Mock branch group expanded")).not.toBeInTheDocument(); expect(screen.getByText("Mock Branch Group BG-1")).toBeInTheDocument(); }); });