FN-5648: show in-progress feedback while merging PRs

Fusion-Task-Id: FN-5648

Fusion-Task-Lineage: f056a665-4d52-4472-8495-487a1865aea4
This commit is contained in:
gsxdsm
2026-05-28 23:31:03 -07:00
parent f5bf7b9b60
commit 41ee331429
4 changed files with 56 additions and 2 deletions

View File

@@ -173,6 +173,15 @@
text-decoration: underline;
}
.pr-hint--info {
background: color-mix(in srgb, var(--color-info) 12%, transparent);
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-info) 35%, transparent);
border-radius: var(--radius-md);
color: var(--color-info);
font-size: 0.8125rem;
padding: var(--space-md);
}
.pr-hint--warning {
background: color-mix(in srgb, var(--color-warning) 12%, transparent);
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-warning) 35%, transparent);

View File

@@ -269,7 +269,7 @@ function PrCard({
</div>
{showMergeControls ? (
<div className="pr-panel-section"><div className="pr-panel-row-label">Merge</div><div className="pr-merge-controls"><select className="select" value={mergeStrategy} onChange={(event) => setMergeStrategy(event.target.value as "merge" | "squash" | "rebase")}><option value="merge">merge</option><option value="squash">squash</option><option value="rebase">rebase</option></select><button className="btn btn-primary btn-sm" onClick={handleMerge} disabled={!mergeReady || isMerging} title={mergeReady ? "Merge pull request" : blockingReasonsTitle || "Refresh PR status to check merge readiness"}>Merge pull request</button><label className="checkbox-label"><input type="checkbox" checked={Boolean(prInfo.autoMergeOnGreen)} onChange={(event) => { void handleAutoMergeToggle(event.currentTarget.checked); }} />Auto-merge when green</label></div>{prInfo.lastMergeError ? <div className="pr-merge-error"><span>{prInfo.lastMergeError}</span><button className="btn btn-sm" onClick={handleMerge} disabled={isMerging}>Retry</button></div> : null}</div>
<div className="pr-panel-section"><div className="pr-panel-row-label">Merge</div><div className="pr-merge-controls"><select className="select" value={mergeStrategy} disabled={isMerging} onChange={(event) => setMergeStrategy(event.target.value as "merge" | "squash" | "rebase")}><option value="merge">merge</option><option value="squash">squash</option><option value="rebase">rebase</option></select><button className="btn btn-primary btn-sm" onClick={handleMerge} disabled={!mergeReady || isMerging} aria-busy={isMerging} data-testid="pr-merge-button" title={mergeReady ? "Merge pull request" : blockingReasonsTitle || "Refresh PR status to check merge readiness"}>{isMerging ? <><RefreshCw size={14} className="spin" />Merging</> : "Merge pull request"}</button><label className="checkbox-label"><input type="checkbox" checked={Boolean(prInfo.autoMergeOnGreen)} disabled={isMerging} onChange={(event) => { void handleAutoMergeToggle(event.currentTarget.checked); }} />Auto-merge when green</label></div>{isMerging ? <div className="pr-hint pr-hint--info" role="status" aria-live="polite">Merging pull request</div> : null}{prInfo.lastMergeError ? <div className="pr-merge-error"><span>{prInfo.lastMergeError}</span><button className="btn btn-sm" onClick={handleMerge} disabled={isMerging}>Retry</button></div> : null}</div>
) : null}
{showConflictHint ? <div className="pr-hint pr-hint--conflict">Merge conflict detected. Resolve/rebase branch and retry reclaim.<button className="btn btn-sm" onClick={async () => { setIsReclaimingConflict(true); try { const result = await reclaimPrConflict(taskId, projectId); if (result.queued) { addToast("Conflict reclaim queued", "success"); const updated = await refreshPrStatus(taskId, projectId); setRefreshState(updated); const targetPr = (updated.all?.map((entry) => entry.prInfo) ?? [updated.prInfo]).find((entry) => entry.number === prInfo.number); if (targetPr) onPrUpdated(targetPr); } else { addToast(result.reason ?? "Conflict reclaim unavailable", "warning"); } } catch (err) { addToast(getErrorMessage(err) || "Failed to queue conflict reclaim", "error"); } finally { setIsReclaimingConflict(false); } }} disabled={isReclaimingConflict}>Retry conflict reclaim</button></div> : null}

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PrInfo } from "../../api";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { PrPanel } from "../PrPanel";
import { mergePr, refreshPrStatus } from "../../api";
@@ -147,6 +148,50 @@ describe("PrPanel merge controls", () => {
expect(screen.getByRole("button", { name: "Merge pull request" })).toBeDisabled();
});
it("shows in-progress merge feedback until merge resolves", async () => {
let resolveMerge: ((value: { prInfo: PrInfo }) => void) | undefined;
(mergePr as ReturnType<typeof vi.fn>).mockImplementation(() => new Promise((resolve) => {
resolveMerge = resolve;
}));
const addToast = vi.fn();
render(
<PrPanel
taskId="FN-1"
prAuthAvailable
onPrUpdated={() => {}}
addToast={addToast}
prInfo={{ url: "https://github.com/o/r/pull/1", number: 1, status: "open", title: "t", headBranch: "h", baseBranch: "main", commentCount: 0, mergeable: "clean" }}
/>,
);
fireEvent.click(screen.getByTestId("pr-merge-button"));
const mergeButton = screen.getByRole("button", { name: /merging/i });
expect(mergeButton).toBeDisabled();
expect(mergeButton).toHaveAttribute("aria-busy", "true");
expect(screen.getByRole("status")).toHaveTextContent("Merging pull request…");
expect(screen.getByRole("combobox")).toBeDisabled();
resolveMerge?.({
prInfo: {
url: "https://github.com/o/r/pull/1",
number: 1,
status: "merged",
title: "t",
headBranch: "h",
baseBranch: "main",
commentCount: 0,
},
});
await waitFor(() => {
expect(screen.getByRole("button", { name: "Merge pull request" })).toBeInTheDocument();
});
expect(screen.queryByText("Merging pull request…")).not.toBeInTheDocument();
expect(addToast).toHaveBeenCalledWith("Pull request merged", "success");
});
it("shows error block and retry", () => {
render(<PrPanel taskId="FN-1" prAuthAvailable onPrUpdated={() => {}} addToast={() => {}} prInfo={{ url: "https://github.com/o/r/pull/1", number: 1, status: "open", title: "t", headBranch: "h", baseBranch: "main", commentCount: 0, lastMergeError: "boom" }} />);
expect(screen.getByText("boom")).toBeInTheDocument();