FN-6043: default GitHub imports to origin remote

Prefer the origin remote when preselecting GitHub imports in the dashboard modal.

- default the GitHub import modal to the origin remote when multiple remotes are available
- keep the remote picker unselected when multiple remotes exist without an origin entry
- expand dashboard tests to cover origin defaults for issues, pull requests, and mobile flows

Files changed:
 .../dashboard/app/components/GitHubImportModal.tsx | 16 ++--
 .../__tests__/GitHubImportModal.test.tsx           | 95 ++++++++++++++++------
 2 files changed, 77 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-6043

Fusion-Task-Lineage: b03b621d-757a-422c-b31c-ca0c34c3c168
This commit is contained in:
gsxdsm
2026-06-09 00:18:08 -07:00
parent cb2b64e6f0
commit 815ec58abd
2 changed files with 78 additions and 35 deletions

View File

@@ -143,14 +143,16 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
setRemotes(fetchedRemotes);
setLoadingRemotes(false);
if (fetchedRemotes.length === 1) {
// Single remote: auto-select it
const remote = fetchedRemotes[0];
setOwner(remote.owner);
setRepo(remote.repo);
setSelectedRemoteName(remote.name);
const defaultRemote = fetchedRemotes.length === 1
? fetchedRemotes[0]
: fetchedRemotes.find((remote) => remote.name === "origin");
if (defaultRemote) {
setOwner(defaultRemote.owner);
setRepo(defaultRemote.repo);
setSelectedRemoteName(defaultRemote.name);
} else if (fetchedRemotes.length > 1) {
// Multiple remotes: don't auto-select, user must choose
// Multiple remotes without origin: don't auto-select, user must choose.
setOwner("");
setRepo("");
setSelectedRemoteName("");

View File

@@ -61,6 +61,11 @@ const multipleRemotes: GitRemote[] = [
{ name: "upstream", owner: "upstream", repo: "kb", url: "https://github.com/upstream/kb.git" },
];
const multipleRemotesWithoutOrigin: GitRemote[] = [
{ name: "upstream", owner: "upstream", repo: "kb", url: "https://github.com/upstream/kb.git" },
{ name: "fork", owner: "dustinbyrne", repo: "kb", url: "https://github.com/dustinbyrne/kb.git" },
];
const mockPulls = [
{ number: 1, title: "Test PR", body: "PR body", html_url: "https://github.com/owner/repo/pull/1", headBranch: "feature", baseBranch: "main" },
{ number: 2, title: "Another PR", body: "Another PR body", html_url: "https://github.com/owner/repo/pull/2", headBranch: "bugfix", baseBranch: "main" },
@@ -289,19 +294,7 @@ describe("GitHubImportModal", () => {
});
});
it("disables Load button when no remote is selected", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotes);
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
expect(screen.getByRole("combobox")).toBeTruthy();
});
const loadButton = screen.getByRole("button", { name: /Load issues/i }) as HTMLButtonElement;
expect(loadButton.disabled).toBe(true);
});
it("enables Load button and auto-loads after selecting a remote", async () => {
it("defaults to origin and auto-loads when multiple remotes include origin", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotes);
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([
{ number: 1, title: "Auto-loaded from origin", body: "", html_url: "https://github.com/dustinbyrne/kb/issues/1", labels: [] },
@@ -310,21 +303,30 @@ describe("GitHubImportModal", () => {
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
expect(screen.getByRole("combobox")).toBeTruthy();
});
fireEvent.change(screen.getByRole("combobox"), { target: { value: "origin" } });
await waitFor(() => {
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("origin");
expect(apiFetchGitHubIssues).toHaveBeenCalledWith("dustinbyrne", "kb", 30, undefined);
expect(screen.getByText("Auto-loaded from origin")).toBeTruthy();
});
// After loading completes, button should be enabled
const loadButton = screen.getByRole("button", { name: /Load issues/i }) as HTMLButtonElement;
expect(loadButton.disabled).toBe(false);
});
it("keeps placeholder selected and does not auto-load when multiple remotes omit origin", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotesWithoutOrigin);
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("");
});
const loadButton = screen.getByRole("button", { name: /Load issues/i }) as HTMLButtonElement;
expect(loadButton.disabled).toBe(true);
expect(apiFetchGitHubIssues).not.toHaveBeenCalled();
});
it("switches owner/repo and auto-loads when changing remote selection", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotes);
vi.mocked(apiFetchGitHubIssues)
@@ -334,18 +336,13 @@ describe("GitHubImportModal", () => {
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
expect(screen.getByRole("combobox")).toBeTruthy();
});
const select = screen.getByRole("combobox");
fireEvent.change(select, { target: { value: "origin" } });
await waitFor(() => {
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("origin");
expect(apiFetchGitHubIssues).toHaveBeenCalledWith("dustinbyrne", "kb", 30, undefined);
expect(screen.getByText("Issue from origin")).toBeTruthy();
});
fireEvent.change(select, { target: { value: "upstream" } });
fireEvent.change(screen.getByRole("combobox"), { target: { value: "upstream" } });
await waitFor(() => {
expect(apiFetchGitHubIssues).toHaveBeenLastCalledWith("upstream", "kb", 30, undefined);
@@ -574,6 +571,31 @@ describe("GitHubImportModal", () => {
window.dispatchEvent(new Event("resize"));
});
it("defaults to origin and auto-loads on mobile", async () => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 640,
});
window.dispatchEvent(new Event("resize"));
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotes);
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([
{ number: 1, title: "Mobile origin issue", body: "", html_url: "https://github.com/dustinbyrne/kb/issues/1", labels: [] },
]);
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("origin");
expect(apiFetchGitHubIssues).toHaveBeenCalledWith("dustinbyrne", "kb", 30, undefined);
expect(screen.getByText("Mobile origin issue")).toBeTruthy();
});
expect(screen.getByTestId("github-import-list-pane").classList.contains("mobile")).toBe(true);
});
it("shows back button in preview header when on mobile", async () => {
// Set mobile viewport
Object.defineProperty(window, "innerWidth", {
@@ -884,6 +906,25 @@ describe("GitHubImportModal", () => {
});
});
it("uses the default origin remote when switching to Pull Requests", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(multipleRemotes);
vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce(mockPulls);
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
await waitFor(() => {
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("origin");
});
fireEvent.click(screen.getByRole("tab", { name: /Pull Requests/i }));
await waitFor(() => {
expect(apiFetchGitHubPulls).toHaveBeenCalledWith("dustinbyrne", "kb", 30);
expect(screen.getByText("Test PR")).toBeTruthy();
});
});
it("displays PR list with branch info", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce(mockPulls);