feat(web): show file statuses in review (#1683)

* feat(web): show file statuses in review

* fix(web): address review status feedback

* fix(web): deduplicate review sources

* fix(web): align review progress identities

* fix(web): guard review status labels

* fix(web): enforce file status exhaustiveness

* perf(web): index review progress sources

* fix(web): preserve files during status hydration

* fix(web): align multi-repo review progress
This commit is contained in:
José Almeida
2026-07-15 12:54:43 +01:00
committed by GitHub
parent abe4147d07
commit e067e7a1f6
39 changed files with 2593 additions and 260 deletions
@@ -0,0 +1,38 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import type { FileChangeStatus } from "@/lib/utils/file-change-status";
import { FileStatusIcon } from "./file-status-icon";
afterEach(cleanup);
describe("FileStatusIcon", () => {
it.each<[FileChangeStatus, string, string]>([
["added", "Added", "tabler-icon-plus"],
["untracked", "Untracked", "tabler-icon-plus"],
["modified", "Modified", "tabler-icon-circle-filled"],
["deleted", "Deleted", "tabler-icon-minus"],
["renamed", "Moved", "tabler-icon-arrow-right"],
])("renders an accessible %s marker", (status, label, glyphClass) => {
render(<FileStatusIcon status={status} />);
const marker = screen.getByRole("img", { name: label });
expect(marker.getAttribute("data-file-status")).toBe(status);
expect(marker.getAttribute("title")).toBe(label);
expect(marker.querySelector(`.${glyphClass}`)).not.toBeNull();
});
it("exposes previous-path context for a moved file", () => {
render(<FileStatusIcon status="renamed" oldPath="src/old-name.ts" />);
const marker = screen.getByRole("img", { name: "Moved from src/old-name.ts" });
expect(marker.getAttribute("title")).toBe("Moved from src/old-name.ts");
});
it("merges caller classes into the fixed-size marker", () => {
render(<FileStatusIcon status="modified" className="custom-marker" />);
const marker = screen.getByRole("img", { name: "Modified" });
expect(marker.className).toContain("shrink-0");
expect(marker.className).toContain("custom-marker");
});
});
@@ -0,0 +1,57 @@
"use client";
import { IconArrowRight, IconCircleFilled, IconMinus, IconPlus } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { fileChangeStatusLabel, type FileChangeStatus } from "@/lib/utils/file-change-status";
type FileStatusIconProps = {
status: FileChangeStatus;
oldPath?: string;
className?: string;
};
const markerClassName: Record<FileChangeStatus, string> = {
added: "border-emerald-600 text-emerald-600",
untracked: "border-emerald-600 text-emerald-600",
modified: "border-amber-600 text-amber-600",
deleted: "border-rose-600 text-rose-600",
renamed: "border-purple-600 text-purple-600",
};
function StatusGlyph({ status }: { status: FileChangeStatus }) {
switch (status) {
case "added":
case "untracked":
return <IconPlus aria-hidden="true" className="h-2 w-2" />;
case "modified":
return <IconCircleFilled aria-hidden="true" className="h-1 w-1" />;
case "deleted":
return <IconMinus aria-hidden="true" className="h-2 w-2" />;
case "renamed":
return <IconArrowRight aria-hidden="true" className="h-2 w-2" />;
default: {
const exhaustiveStatus: never = status;
return exhaustiveStatus;
}
}
}
export function FileStatusIcon({ status, oldPath, className }: FileStatusIconProps) {
const label = fileChangeStatusLabel(status, oldPath);
return (
<span
role="img"
aria-label={label}
title={label}
data-file-status={status}
className={cn(
"flex h-3 w-3 shrink-0 items-center justify-center rounded border",
markerClassName[status],
className,
)}
>
<StatusGlyph status={status} />
</span>
);
}