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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7319-compact-branch-groups.md
Normal file
7
.changeset/fn-7319-compact-branch-groups.md
Normal file
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) {
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<section className="card branch-group-card">
|
||||
<section className={`card branch-group-card${collapsed ? " branch-group-card--collapsed" : ""}`}>
|
||||
<header className="branch-group-card-header">
|
||||
<div className="branch-group-card-title">
|
||||
<GitBranch size={14} />
|
||||
|
||||
@@ -3115,7 +3115,8 @@ export function TaskDetailContent({
|
||||
</div>
|
||||
</div>
|
||||
{task.branchContext?.groupId && (
|
||||
<BranchGroupCard groupId={task.branchContext.groupId} projectId={projectId} />
|
||||
/* 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. */
|
||||
<BranchGroupCard key={`${task.id}:${task.branchContext.groupId}`} groupId={task.branchContext.groupId} projectId={projectId} />
|
||||
)}
|
||||
{/* FNXC:Workspace 2026-06-21-00:00: workspace tasks have no singular
|
||||
task.worktree/task.branch; surface their acquired per-sub-repo worktrees
|
||||
|
||||
@@ -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<string, unknown> = {}) {
|
||||
return {
|
||||
id: "BG-1",
|
||||
@@ -59,6 +67,7 @@ describe("BranchGroupCard", () => {
|
||||
apiGetBranchGroup.mockResolvedValue({ group: makeGroup() });
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
await expandBranchGroup();
|
||||
const button = await screen.findByRole("button", { name: /open pr/i });
|
||||
fireEvent.click(button);
|
||||
|
||||
@@ -87,6 +97,7 @@ describe("BranchGroupCard", () => {
|
||||
});
|
||||
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
await expandBranchGroup();
|
||||
const abandon = await screen.findByRole("button", { name: /abandon group/i });
|
||||
fireEvent.click(abandon);
|
||||
|
||||
@@ -137,6 +150,7 @@ describe("BranchGroupCard", () => {
|
||||
});
|
||||
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
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(<BranchGroupCard groupId="BG-1" />);
|
||||
|
||||
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*\{(?<block>[^}]*)\}/)?.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*\{(?<block>[^}]*)\}/)?.groups?.block ?? "";
|
||||
expect(titleBlock).toContain("font-size: var(--font-size-xs)");
|
||||
expect(titleBlock).toContain("line-height: var(--line-height-tight)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,16 @@ import {
|
||||
import { TaskDetailContent, TaskDetailModal } from "../TaskDetailModal";
|
||||
|
||||
vi.mock("../BranchGroupCard", () => ({
|
||||
BranchGroupCard: ({ groupId }: { groupId: string }) => <div>Mock Branch Group {groupId}</div>,
|
||||
BranchGroupCard: ({ groupId }: { groupId: string }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
return (
|
||||
<div>
|
||||
Mock Branch Group {groupId}
|
||||
<button type="button" onClick={() => setExpanded(true)}>Mock expand branch group</button>
|
||||
{expanded && <span>Mock branch group expanded</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
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 (
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ branchContext: { groupId: "BG-1", source: "planning", assignmentMode: "shared" } })}
|
||||
task={makeTask({ id, branchContext })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user