7c00767b6e
The Changes tab in the dockview panel (the Files / Changes (N) tab the user actually interacts with — not the standalone Review dialog) was still rendering a flat file list, so multi-repo task changes either collided onto the legacy single-status (showing only one repo's files) or rendered without grouping. - FileInfo gains repository_name (optional, single-repo unchanged). - useSessionGit aggregates files from useSessionGitStatusByRepo when multi-repo data is present, stamping each FileInfo with its repository_name. Falls back to the legacy single status for single-repo workspaces. Aggregation extracted into aggregateFilesAcrossRepos to keep useSessionGit under the 100-line function lint cap. - ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries repositoryName through to the renderer. - FileListSection delegates to a new FileListBody that groups rows by repositoryName; renders a small per-repo subheader when 2+ repos OR any single named repo is present, otherwise falls back to the pre-multi-repo flat list (same DOM shape). - Tests cover: no header for plain (no repo_name) files, headers per repo for 2+ groups, single named repo still gets a label. Restart the backend so the per-repo trackers spawn for in-flight multi-repo tasks; new launches are unaffected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { render, screen, cleanup } from "@testing-library/react";
|
|
import type { ComponentProps } from "react";
|
|
|
|
vi.mock("./changes-panel-file-row", () => ({
|
|
FileRow: ({ file }: { file: { path: string } }) => (
|
|
<li data-testid="file-row">{file.path}</li>
|
|
),
|
|
BulkActionBar: () => null,
|
|
DefaultActionButtons: () => null,
|
|
}));
|
|
|
|
vi.mock("./commit-row", () => ({
|
|
CommitRow: () => null,
|
|
}));
|
|
|
|
vi.mock("@/hooks/use-multi-select", () => ({
|
|
useMultiSelect: () => ({
|
|
selectedPaths: new Set<string>(),
|
|
isSelected: () => false,
|
|
handleClick: vi.fn(),
|
|
clearSelection: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
import { FileListSection } from "./changes-panel-timeline";
|
|
|
|
afterEach(cleanup);
|
|
|
|
type Props = ComponentProps<typeof FileListSection>;
|
|
|
|
const baseProps: Omit<Props, "files" | "variant" | "isLast" | "actionLabel" | "onAction"> = {
|
|
pendingStageFiles: new Set(),
|
|
onOpenDiff: vi.fn(),
|
|
onEditFile: vi.fn(),
|
|
onStage: vi.fn(),
|
|
onUnstage: vi.fn(),
|
|
onDiscard: vi.fn(),
|
|
};
|
|
|
|
function file(path: string, repo?: string): Props["files"][number] {
|
|
return {
|
|
path,
|
|
status: "modified",
|
|
staged: false,
|
|
plus: 1,
|
|
minus: 0,
|
|
oldPath: undefined,
|
|
repositoryName: repo,
|
|
};
|
|
}
|
|
|
|
describe("FileListSection — multi-repo grouping", () => {
|
|
it("renders no repo header when files lack a repository_name (single-repo)", () => {
|
|
render(
|
|
<FileListSection
|
|
{...baseProps}
|
|
variant="unstaged"
|
|
isLast={false}
|
|
actionLabel="Stage all"
|
|
onAction={() => undefined}
|
|
files={[file("a.ts"), file("b.ts")]}
|
|
/>,
|
|
);
|
|
expect(screen.queryAllByTestId("changes-repo-header")).toHaveLength(0);
|
|
expect(screen.getAllByTestId("file-row")).toHaveLength(2);
|
|
});
|
|
|
|
it("renders one header per repo when 2+ repos are present", () => {
|
|
render(
|
|
<FileListSection
|
|
{...baseProps}
|
|
variant="unstaged"
|
|
isLast={false}
|
|
actionLabel="Stage all"
|
|
onAction={() => undefined}
|
|
files={[
|
|
file("src/app.tsx", "frontend"),
|
|
file("src/api.ts", "frontend"),
|
|
file("handlers/task.go", "backend"),
|
|
]}
|
|
/>,
|
|
);
|
|
const headers = screen.getAllByTestId("changes-repo-header");
|
|
expect(headers).toHaveLength(2);
|
|
expect(headers[0].textContent).toContain("frontend");
|
|
expect(headers[0].textContent).toContain("2");
|
|
expect(headers[1].textContent).toContain("backend");
|
|
expect(headers[1].textContent).toContain("1");
|
|
});
|
|
|
|
it("shows a header for a single named repo too", () => {
|
|
render(
|
|
<FileListSection
|
|
{...baseProps}
|
|
variant="unstaged"
|
|
isLast={false}
|
|
actionLabel="Stage all"
|
|
onAction={() => undefined}
|
|
files={[file("a.ts", "only-repo")]}
|
|
/>,
|
|
);
|
|
expect(screen.getByTestId("changes-repo-header").textContent).toContain("only-repo");
|
|
});
|
|
});
|