diff --git a/.changeset/fn-7442-github-import-return-list.md b/.changeset/fn-7442-github-import-return-list.md
new file mode 100644
index 0000000000..731fe6e326
--- /dev/null
+++ b/.changeset/fn-7442-github-import-return-list.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Return GitHub issue import actions to the main issue list.
+category: fix
+dev: Updates Import Tasks issue import/close navigation and regression coverage.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 847a659883..af186da21e 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -207,7 +207,7 @@ Use Import Tasks on desktop/tablet:
4. Select an issue or pull request row.
Expected outcome: the preview pane shows its title, source link, body excerpt/content, labels or PR metadata, and import availability.
5. Select the import action.
- Expected outcome: Fusion creates a task (or review task for a pull request) on the board and preserves GitHub provenance/tracking metadata.
+ Expected outcome: Fusion creates a task (or review task for a pull request) on the board and preserves GitHub provenance/tracking metadata. After a successful issue import, the issue selection clears and the view returns to the main issue list/no-selection preview so completed issue actions do not leave stale buttons active.
Use GitHub import on mobile:
diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx
index 612082ec4e..7d9cc8f575 100644
--- a/packages/dashboard/app/components/GitHubImportModal.tsx
+++ b/packages/dashboard/app/components/GitHubImportModal.tsx
@@ -842,6 +842,16 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
setMobileView('list');
}, []);
+ /**
+ * FNXC:GitHubImport 2026-07-02-00:00:
+ * Successful GitHub issue import/close actions must return users to the main issue list instead of leaving a completed issue's preview, action buttons, or selected radio active.
+ * Failures intentionally do not call this helper so the selected preview remains available for retry with the existing error affordance.
+ */
+ const returnToIssueListAfterSuccess = useCallback(() => {
+ setSelectedIssueNumber(null);
+ setMobileView("list");
+ }, []);
+
const handleImport = useCallback(async () => {
if (activeTab === "issues") {
if (selectedIssueNumber === null) return;
@@ -852,10 +862,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
try {
const task = await apiImportGitHubIssue(owner.trim(), repo.trim(), selectedIssueNumber, projectId);
onImport(task);
- setSelectedIssueNumber(null);
- if (isMobile && mobileView === "preview") {
- setMobileView("list");
- }
+ returnToIssueListAfterSuccess();
} catch (err) {
const msg = getErrorMessage(err);
if (msg?.includes("already imported")) {
@@ -890,7 +897,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
setImporting(false);
}
}
- }, [activeTab, selectedIssueNumber, selectedPullNumber, owner, repo, projectId, onImport, isMobile, mobileView]);
+ }, [activeTab, selectedIssueNumber, selectedPullNumber, owner, repo, projectId, onImport, isMobile, mobileView, returnToIssueListAfterSuccess]);
/*
FNXC:GitHubImport 2026-06-23-01:00:
@@ -995,13 +1002,14 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
return next;
});
setCloseToast({ type: "success", message: t("git.issueClosedToast", "Issue #{{number}} closed", { number: issueNumber }) });
+ returnToIssueListAfterSuccess();
} catch (err: unknown) {
setCloseToast({ type: "error", message: getErrorMessage(err) });
} finally {
setClosingIssue(false);
closeToastTimerRef.current = setTimeout(() => setCloseToast(null), 4000);
}
- }, [selectedIssueNumber, owner, repo, t]);
+ }, [selectedIssueNumber, owner, repo, t, returnToIssueListAfterSuccess]);
const selectedIssue = issues.find((i) => i.number === selectedIssueNumber);
const selectedPull = pulls.find((p) => p.number === selectedPullNumber);
diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
index 2e31fe917c..aaf9d6c3c7 100644
--- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
@@ -714,9 +714,9 @@ describe("GitHubImportModal", () => {
});
});
- it("stays open and resets selection after successful issue import", async () => {
+ it("stays open and returns desktop issue imports to the no-selection list state", async () => {
const issues = [
- { number: 1, title: "First Issue", body: "Body 1", html_url: "https://github.com/owner/repo/issues/1", labels: [] },
+ { number: 1, title: "First Issue", body: null, html_url: "https://github.com/owner/repo/issues/1", labels: [] },
];
vi.mocked(fetchGitRemotes).mockResolvedValueOnce([{ name: "origin", owner: "owner", repo: "repo", url: "" }]);
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
@@ -731,7 +731,9 @@ describe("GitHubImportModal", () => {
});
const importButton = screen.getByTestId("github-import-action-top") as HTMLButtonElement;
- fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i }));
+ const radio = screen.getByRole("radio", { name: /Select issue #1/i }) as HTMLInputElement;
+ fireEvent.click(radio);
+ expect(await screen.findByTestId("github-import-preview-card")).toBeTruthy();
expect(importButton.disabled).toBe(false);
fireEvent.click(importButton);
@@ -739,9 +741,9 @@ describe("GitHubImportModal", () => {
await waitFor(() => {
expect(apiImportGitHubIssue).toHaveBeenCalledWith("owner", "repo", 1, "project-1");
expect(onClose).not.toHaveBeenCalled();
- });
-
- await waitFor(() => {
+ expect(radio.checked).toBe(false);
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByTestId("github-import-preview-empty")).toHaveTextContent("No issue selected");
expect((screen.getByTestId("github-import-action-top") as HTMLButtonElement).disabled).toBe(true);
});
@@ -755,6 +757,57 @@ describe("GitHubImportModal", () => {
});
});
+ it("returns modal bottom issue imports to the list without dismissing the modal", async () => {
+ const issues = [
+ { number: 2, title: "Bottom Action Issue", body: "Body", html_url: "https://github.com/owner/repo/issues/2", labels: [] },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce([{ name: "origin", owner: "owner", repo: "repo", url: "" }]);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+ vi.mocked(apiImportGitHubIssue).mockResolvedValueOnce({ ...mockTask, id: "FN-002" });
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Bottom Action Issue")).toBeTruthy());
+ fireEvent.click(screen.getByRole("radio", { name: /Select issue #2/i }));
+ expect(await screen.findByTestId("github-import-preview-card")).toBeTruthy();
+
+ const bottomImportButton = screen.getAllByRole("button", { name: "Import" }).at(-1) as HTMLButtonElement;
+ fireEvent.click(bottomImportButton);
+
+ await waitFor(() => {
+ expect(apiImportGitHubIssue).toHaveBeenCalledWith("owner", "repo", 2, "project-1");
+ expect(onClose).not.toHaveBeenCalled();
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByTestId("github-import-preview-empty")).toHaveTextContent("No issue selected");
+ expect((screen.getByRole("radio", { name: /Select issue #2/i }) as HTMLInputElement).checked).toBe(false);
+ });
+ });
+
+ it("keeps the selected issue preview open when issue import fails", async () => {
+ const issues = [
+ { number: 3, title: "Retry Issue", body: "Retry body", html_url: "https://github.com/owner/repo/issues/3", labels: [] },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce([{ name: "origin", owner: "owner", repo: "repo", url: "" }]);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+ vi.mocked(apiImportGitHubIssue).mockRejectedValueOnce(new Error("already imported elsewhere"));
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Retry Issue")).toBeTruthy());
+ const radio = screen.getByRole("radio", { name: /Select issue #3/i }) as HTMLInputElement;
+ fireEvent.click(radio);
+ expect(await screen.findByTestId("github-import-preview-card")).toHaveTextContent("Retry Issue");
+
+ fireEvent.click(screen.getByTestId("github-import-action-top"));
+
+ await waitFor(() => {
+ expect(screen.getByText("already imported elsewhere")).toBeTruthy();
+ expect(radio.checked).toBe(true);
+ expect(screen.getByTestId("github-import-preview-card")).toHaveTextContent("Retry Issue");
+ expect(screen.queryByTestId("github-import-preview-empty")).toBeNull();
+ });
+ });
+
it("shows 'Imported' badge for already imported issues", async () => {
const existingTask: Task = {
...mockTask,
@@ -967,6 +1020,108 @@ describe("GitHubImportModal", () => {
expect(previewPane.classList.contains("active")).toBe(false);
});
+ it("returns mobile issue imports from preview to the active list pane", async () => {
+ Object.defineProperty(window, "innerWidth", {
+ writable: true,
+ configurable: true,
+ value: 480,
+ });
+ window.dispatchEvent(new Event("resize"));
+
+ const issues = [
+ { number: 1, title: "Mobile Import Issue", body: "Body", html_url: "https://github.com/owner/repo/issues/1", labels: [] },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+ vi.mocked(apiImportGitHubIssue).mockResolvedValueOnce(mockTask);
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Mobile Import Issue")).toBeTruthy());
+ fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i }));
+
+ const previewPane = screen.getByTestId("github-import-preview-pane");
+ const listPane = screen.getByTestId("github-import-list-pane");
+ await waitFor(() => expect(previewPane.classList.contains("active")).toBe(true));
+ expect(listPane.classList.contains("active")).toBe(false);
+
+ fireEvent.click(screen.getByTestId("github-import-action-top"));
+
+ await waitFor(() => {
+ expect(apiImportGitHubIssue).toHaveBeenCalledWith("dustinbyrne", "kb", 1, undefined);
+ expect(previewPane.classList.contains("active")).toBe(false);
+ expect(listPane.classList.contains("active")).toBe(true);
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByTestId("github-import-preview-empty")).toHaveTextContent("No issue selected");
+ });
+ });
+
+ it("returns mobile issue close from preview to the active list pane", async () => {
+ Object.defineProperty(window, "innerWidth", {
+ writable: true,
+ configurable: true,
+ value: 480,
+ });
+ window.dispatchEvent(new Event("resize"));
+
+ const issues = [
+ { number: 4, title: "Mobile Close Issue", body: "Body", html_url: "https://github.com/owner/repo/issues/4", labels: [], state: "open" as const },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Mobile Close Issue")).toBeTruthy());
+ fireEvent.click(screen.getByRole("radio", { name: /Select issue #4/i }));
+
+ const previewPane = screen.getByTestId("github-import-preview-pane");
+ const listPane = screen.getByTestId("github-import-list-pane");
+ await waitFor(() => expect(previewPane.classList.contains("active")).toBe(true));
+
+ fireEvent.click(await screen.findByTestId("github-import-issue-close"));
+
+ await waitFor(() => {
+ expect(apiCloseGitHubIssue).toHaveBeenCalledWith("dustinbyrne/kb", 4);
+ expect(previewPane.classList.contains("active")).toBe(false);
+ expect(listPane.classList.contains("active")).toBe(true);
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByTestId("github-import-preview-empty")).toHaveTextContent("No issue selected");
+ });
+ expect(await screen.findByTestId("github-import-issue-close-toast")).toHaveTextContent("Issue #4 closed");
+ });
+
+ it("keeps the mobile issue preview active when import fails", async () => {
+ Object.defineProperty(window, "innerWidth", {
+ writable: true,
+ configurable: true,
+ value: 480,
+ });
+ window.dispatchEvent(new Event("resize"));
+
+ const issues = [
+ { number: 6, title: "Mobile Retry Issue", body: "Body", html_url: "https://github.com/owner/repo/issues/6", labels: [] },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+ vi.mocked(apiImportGitHubIssue).mockRejectedValueOnce(new Error("Duplicate server error"));
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Mobile Retry Issue")).toBeTruthy());
+ fireEvent.click(screen.getByRole("radio", { name: /Select issue #6/i }));
+ const previewPane = screen.getByTestId("github-import-preview-pane");
+ await waitFor(() => expect(previewPane.classList.contains("active")).toBe(true));
+
+ fireEvent.click(screen.getByTestId("github-import-action-top"));
+
+ await waitFor(() => {
+ expect(screen.getByText("Duplicate server error")).toBeTruthy();
+ expect(previewPane.classList.contains("active")).toBe(true);
+ expect(screen.getByTestId("github-import-preview-card")).toHaveTextContent("Mobile Retry Issue");
+ });
+ });
+
it("renders long selected issue body in full on mobile without a truncation ellipsis", async () => {
Object.defineProperty(window, "innerWidth", {
writable: true,
@@ -1263,8 +1418,8 @@ describe("GitHubImportModal", () => {
});
});
- // FNXC:GitHubImport 2026-06-23-03:15: The Close issue button calls the close API and reflects the closed state locally without dismissing the preview.
- it("closes the selected issue via the close API and reflects the closed state", async () => {
+ // FNXC:GitHubImport 2026-07-02-00:00: Successful Close issue returns to the issue list/no-selection state; failure stays on the preview so the user can retry.
+ it("closes the selected issue via the close API and returns desktop to the no-selection list state", async () => {
Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1200 });
const issues = [
@@ -1289,20 +1444,47 @@ describe("GitHubImportModal", () => {
expect(vi.mocked(apiCloseGitHubIssue)).toHaveBeenCalledWith("dustinbyrne/kb", 5);
});
- // Success toast surfaces without dismissing the preview.
- expect(await screen.findByTestId("github-import-issue-close-toast")).toBeTruthy();
+ // Success toast surfaces without dismissing the modal, while the completed issue preview is cleared.
+ expect(await screen.findByTestId("github-import-issue-close-toast")).toHaveTextContent("Issue #5 closed");
- // Closed state reflects locally: badge flips to "closed" and the Close button is gone (only OPEN issues show it).
await waitFor(() => {
- const previewCard = screen.getByTestId("github-import-preview-card");
- expect(within(previewCard).getByText("closed")).toBeTruthy();
+ expect((screen.getByRole("radio", { name: /Select issue #5/i }) as HTMLInputElement).checked).toBe(false);
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByTestId("github-import-preview-empty")).toHaveTextContent("No issue selected");
expect(screen.queryByTestId("github-import-issue-close")).toBeNull();
});
- // Preview is NOT dismissed.
expect(onClose).not.toHaveBeenCalled();
});
+ it("keeps the selected issue preview open when close fails", async () => {
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1200 });
+
+ const issues = [
+ { number: 8, title: "Close Retry Issue", body: "Retry close body", html_url: "https://github.com/owner/repo/issues/8", labels: [], state: "open" as const, author: "dave" },
+ ];
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
+ vi.mocked(apiCloseGitHubIssue).mockRejectedValueOnce(new Error("close failed"));
+
+ render();
+
+ await waitFor(() => expect(screen.getByText("Close Retry Issue")).toBeTruthy());
+ const radio = screen.getByRole("radio", { name: /Select issue #8/i }) as HTMLInputElement;
+ fireEvent.click(radio);
+ expect(await screen.findByTestId("github-import-preview-card")).toHaveTextContent("Close Retry Issue");
+
+ fireEvent.click(await screen.findByTestId("github-import-issue-close"));
+
+ await waitFor(() => {
+ expect(apiCloseGitHubIssue).toHaveBeenCalledWith("dustinbyrne/kb", 8);
+ expect(screen.getByTestId("github-import-issue-close-toast")).toHaveTextContent("close failed");
+ expect(radio.checked).toBe(true);
+ expect(screen.getByTestId("github-import-preview-card")).toHaveTextContent("Close Retry Issue");
+ expect(screen.getByTestId("github-import-issue-close")).toBeTruthy();
+ });
+ });
+
// FNXC:GitHubImport 2026-06-22-18:30: Desktop preview must show the FULL issue/PR body (no 200-char clamp). The list response already carries the complete body, so no detail fetch is needed.
it("renders long selected issue body in full on desktop without a truncation ellipsis", async () => {
Object.defineProperty(window, "innerWidth", {