feat(FN-855): align Git Manager remote fields with pencil icon edit affordance

- Replace previous edit affordance on remote fields with Pencil icon in GitManagerModal
- Extend GitManagerModal test suite with regression coverage for edit affordances
- Update README to document inline edit controls for Git Manager remotes
This commit is contained in:
gsxdsm
2026-04-04 21:34:35 -07:00
parent f4a002ca46
commit 148b7c1c1e
3 changed files with 163 additions and 7 deletions

View File

@@ -74,6 +74,7 @@ import {
CheckCircle,
XCircle,
Send,
Pencil,
} from "lucide-react";
// ── Types & Constants ─────────────────────────────────────────────
@@ -1953,9 +1954,9 @@ function RemotesPanel({
className="btn btn-icon"
onClick={(e) => { e.stopPropagation(); startEditingName(remote); }}
disabled={remoteActionLoading !== null}
title="Rename remote"
title="Edit remote name"
>
<GitBranchIcon size={12} color="var(--text-muted)" />
<Pencil size={12} color="var(--text-muted)" />
</button>
</div>
)}
@@ -2007,9 +2008,9 @@ function RemotesPanel({
className="btn btn-icon"
onClick={(e) => { e.stopPropagation(); startEditingUrl(remote); }}
disabled={remoteActionLoading !== null}
title="Edit URL"
title="Edit remote URL"
>
<FileEdit size={12} color="var(--text-muted)" />
<Pencil size={12} color="var(--text-muted)" />
</button>
</div>
)}

View File

@@ -989,7 +989,7 @@ describe("GitManagerModal", () => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
const renameButton = screen.getByTitle("Rename remote");
const renameButton = screen.getByTitle("Edit remote name");
await user.click(renameButton);
// The input should appear - find it by its autoFocus or by looking for an input
@@ -1023,7 +1023,7 @@ describe("GitManagerModal", () => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
const editButton = screen.getByTitle("Edit URL");
const editButton = screen.getByTitle("Edit remote URL");
await user.click(editButton);
const urlInput = screen.getByDisplayValue("https://old-url.com/repo.git");
@@ -1040,6 +1040,161 @@ describe("GitManagerModal", () => {
});
});
// ── Edit Affordance Regression ──────────────────────────────────
it("shows editor-style icon for remote name edit action", async () => {
(fetchGitRemotesDetailed as any).mockResolvedValue([
{ name: "origin", fetchUrl: "https://github.com/a/b.git", pushUrl: "https://github.com/a/b.git" },
]);
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// The name edit button should have an accessible title distinguishing it as a name edit
const nameEditBtn = screen.getByTitle("Edit remote name");
expect(nameEditBtn).toBeInTheDocument();
expect(nameEditBtn).toBeVisible();
// Verify it does not propagate clicks to row selection
const remoteItem = nameEditBtn.closest(".gm-remote-item");
const selectedBefore = remoteItem?.classList.contains("selected");
fireEvent.click(nameEditBtn);
// After clicking, the editing state activates (input appears), selection should not change unexpectedly
expect(screen.getByDisplayValue("origin")).toBeInTheDocument();
});
it("shows editor-style icon for remote URL edit action", async () => {
(fetchGitRemotesDetailed as any).mockResolvedValue([
{ name: "origin", fetchUrl: "https://github.com/a/b.git", pushUrl: "https://github.com/a/b.git" },
]);
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// The URL edit button should have an accessible title distinguishing it as a URL edit
const urlEditBtn = screen.getByTitle("Edit remote URL");
expect(urlEditBtn).toBeInTheDocument();
expect(urlEditBtn).toBeVisible();
// Verify it does not propagate clicks to row selection
fireEvent.click(urlEditBtn);
// After clicking, the URL editing state activates
expect(screen.getByDisplayValue("https://github.com/a/b.git")).toBeInTheDocument();
});
it("distinguishes name edit and URL edit controls with unique titles", async () => {
(fetchGitRemotesDetailed as any).mockResolvedValue([
{ name: "origin", fetchUrl: "https://github.com/a/b.git", pushUrl: "https://github.com/a/b.git" },
]);
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// Both edit controls should be present and distinguishable by title
const nameEditBtn = screen.getByTitle("Edit remote name");
const urlEditBtn = screen.getByTitle("Edit remote URL");
expect(nameEditBtn).not.toBe(urlEditBtn);
// Each enters the correct edit mode
fireEvent.click(nameEditBtn);
expect(screen.getByDisplayValue("origin")).toBeInTheDocument();
// Cancel name edit
const cancelBtn = nameEditBtn.closest(".gm-remote-edit")?.querySelector(".btn.btn-sm:not(.btn-primary)");
expect(cancelBtn).toBeTruthy();
fireEvent.click(cancelBtn as HTMLButtonElement);
// Now click URL edit
fireEvent.click(urlEditBtn);
expect(screen.getByDisplayValue("https://github.com/a/b.git")).toBeInTheDocument();
});
it("preserves rename API call behavior through edit affordance", async () => {
(fetchGitRemotesDetailed as any).mockResolvedValue([
{ name: "origin", fetchUrl: "https://github.com/a/b.git", pushUrl: "https://github.com/a/b.git" },
]);
(renameGitRemote as any).mockResolvedValue(undefined);
const user = userEvent.setup();
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// Click the name edit affordance
const nameEditBtn = screen.getByTitle("Edit remote name");
await user.click(nameEditBtn);
// Edit the name and save
const nameInput = screen.getByDisplayValue("origin");
await user.clear(nameInput);
await user.type(nameInput, "upstream");
const saveButton = nameInput.closest(".gm-remote-edit")?.querySelector(".btn.btn-sm.btn-primary");
expect(saveButton).toBeTruthy();
await user.click(saveButton as HTMLButtonElement);
await waitFor(() => {
expect(renameGitRemote).toHaveBeenCalledWith("origin", "upstream");
});
});
it("preserves URL update API call behavior through edit affordance", async () => {
(fetchGitRemotesDetailed as any).mockResolvedValue([
{ name: "origin", fetchUrl: "https://old-url.com/repo.git", pushUrl: "https://old-url.com/repo.git" },
]);
(updateGitRemoteUrl as any).mockResolvedValue(undefined);
const user = userEvent.setup();
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
await waitFor(() => {
expect(screen.getByText("origin")).toBeInTheDocument();
});
// Click the URL edit affordance
const urlEditBtn = screen.getByTitle("Edit remote URL");
await user.click(urlEditBtn);
// Edit the URL and save
const urlInput = screen.getByDisplayValue("https://old-url.com/repo.git");
await user.clear(urlInput);
await user.type(urlInput, "https://new-url.com/repo.git");
const saveButton = urlInput.closest(".gm-remote-edit")?.querySelector(".btn.btn-sm.btn-primary");
expect(saveButton).toBeTruthy();
await user.click(saveButton as HTMLButtonElement);
await waitFor(() => {
expect(updateGitRemoteUrl).toHaveBeenCalledWith("origin", "https://new-url.com/repo.git");
});
});
it("handles API errors gracefully", async () => {
(fetchGitRemotesDetailed as any).mockRejectedValue(new Error("Failed to load remotes"));