fix: move changes stage action to file icon slot (#1349)

This commit is contained in:
Carlos Florêncio
2026-06-12 08:54:50 +01:00
committed by GitHub
parent 66e2ed226d
commit 418beaf11b
2 changed files with 121 additions and 18 deletions
@@ -155,6 +155,74 @@ describe("FileRow hover swap (stats <-> actions occupy same cell)", () => {
});
});
describe("FileRow tree-mode hover stage action", () => {
it("keeps the stage button in the icon slot instead of the right hover actions", () => {
const { container } = render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path: "src/file.go" }}
isPending={false}
treeMode
onSelect={noopSelect}
onOpenDiff={noop}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
const row = container.querySelector("[data-changes-file='src/file.go']") as HTMLElement | null;
expect(row).not.toBeNull();
const iconSlot = row!.querySelector("[data-testid='file-row-icon-action-slot']");
expect(iconSlot).not.toBeNull();
expect(iconSlot!.classList.contains("size-4")).toBe(true);
expect(iconSlot!.querySelector("button[title='Stage file']")).not.toBeNull();
const rightActions = row!.querySelector("[data-testid='file-row-hover-actions']");
expect(rightActions).not.toBeNull();
expect(rightActions!.querySelector("button[title='Stage file']")).toBeNull();
});
it("keeps the unstage button in the same icon slot for staged files", () => {
const { container } = render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path: "src/staged.go", staged: true }}
isPending={false}
treeMode
onSelect={noopSelect}
onOpenDiff={noop}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
const row = container.querySelector(
"[data-changes-file='src/staged.go']",
) as HTMLElement | null;
expect(row).not.toBeNull();
const iconSlot = row!.querySelector("[data-testid='file-row-icon-action-slot']");
expect(iconSlot).not.toBeNull();
expect(iconSlot!.classList.contains("size-4")).toBe(true);
expect(iconSlot!.querySelector("button[title='Unstage file']")).not.toBeNull();
const rightActions = row!.querySelector("[data-testid='file-row-hover-actions']");
expect(rightActions).not.toBeNull();
expect(rightActions!.querySelector("button[title='Unstage file']")).toBeNull();
});
});
describe("FileRow active-tab highlight", () => {
it("renders data-active='true' and bg-accent/60 when isActive", () => {
const { container } = render(
@@ -101,7 +101,15 @@ export function FileRow({
style={indentPx ? { paddingLeft: indentPx } : undefined}
>
{treeMode ? (
<FileIcon fileName={name} className="size-4 shrink-0" />
<TreeModeFileActionSlot
name={name}
isPending={isPending}
staged={file.staged}
path={file.path}
repo={file.repositoryName}
onStage={onStage}
onUnstage={onUnstage}
/>
) : (
<StageButton
isPending={isPending}
@@ -133,24 +141,52 @@ export function FileRow({
repo={file.repositoryName}
onDiscard={onDiscard}
onEditFile={onEditFile}
stageButton={
treeMode ? (
<StageButton
isPending={isPending}
staged={file.staged}
path={file.path}
repo={file.repositoryName}
onStage={onStage}
onUnstage={onUnstage}
/>
) : null
}
/>
</div>
</li>
);
}
function TreeModeFileActionSlot({
name,
isPending,
staged,
path,
repo,
onStage,
onUnstage,
}: {
name: string;
isPending: boolean;
staged: boolean;
path: string;
repo?: string;
onStage: (path: string, repo?: string) => void;
onUnstage: (path: string, repo?: string) => void;
}) {
return (
<div
data-testid="file-row-icon-action-slot"
className="grid size-4 shrink-0 items-center justify-center [&>*]:col-start-1 [&>*]:row-start-1"
>
<FileIcon
fileName={name}
className="size-4 transition-opacity group-hover:opacity-0 pointer-events-none"
/>
<div className="opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
<StageButton
isPending={isPending}
staged={staged}
path={path}
repo={repo}
onStage={onStage}
onUnstage={onUnstage}
/>
</div>
</div>
);
}
function StageButton({
isPending,
staged,
@@ -209,17 +245,17 @@ function FileRowActions({
repo,
onDiscard,
onEditFile,
stageButton,
}: {
path: string;
repo?: string;
onDiscard: (path: string, repo?: string) => void;
onEditFile: (path: string) => void;
/** Tree mode: stage/unstage control rendered alongside the other hover actions. */
stageButton?: React.ReactNode;
}) {
return (
<div className="flex items-center gap-1 justify-end opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none group-hover:pointer-events-auto">
<div
data-testid="file-row-hover-actions"
className="flex items-center gap-1 justify-end opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none group-hover:pointer-events-auto"
>
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -250,7 +286,6 @@ function FileRowActions({
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
{stageButton}
</div>
);
}