From 67281fe4dde9c75f671b9edc933110602dc413b2 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 09:25:26 -0700 Subject: [PATCH 1/2] fix(FN-1721): scope GitHub import remotes to project Pass the active projectId when the GitHub import modal detects remotes so multi-project dashboards do not show a false no-remotes state. Ignore stale remote responses when projectId changes while the modal remains open. References: https://github.com/Runfusion/Fusion/issues/1721 --- .changeset/github-import-project-remotes.md | 5 ++ .../app/components/GitHubImportModal.tsx | 25 ++++++-- .../__tests__/GitHubImportModal.test.tsx | 60 ++++++++++++++++++- 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 .changeset/github-import-project-remotes.md diff --git a/.changeset/github-import-project-remotes.md b/.changeset/github-import-project-remotes.md new file mode 100644 index 0000000000..b960e8fc67 --- /dev/null +++ b/.changeset/github-import-project-remotes.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix Import from GitHub remote detection in multi-project dashboards by passing the active `projectId` to the `/api/git/remotes` lookup. The dialog now lists configured GitHub remotes instead of showing "No GitHub remotes detected" when the backend requires project scope. diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 34fe32d204..4b4dd1e72f 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -86,6 +86,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, const [loadingRemotes, setLoadingRemotes] = useState(false); const [selectedRemoteName, setSelectedRemoteName] = useState(""); const mountedRef = useRef(false); + const remoteLoadRequestIdRef = useRef(0); const modalRef = useRef(null); useModalResizePersist(modalRef, isOpen && resizePersistEnabled, "fusion:github-modal-size"); const overlayDismissProps = useOverlayDismiss(onClose); @@ -153,11 +154,22 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, autoLoadedRef.current = null; mountedRef.current = true; + const remoteLoadRequestId = remoteLoadRequestIdRef.current + 1; + remoteLoadRequestIdRef.current = remoteLoadRequestId; + let cancelled = false; - // Fetch git remotes - fetchGitRemotes() + /* + FNXC:GitHubImport 2026-06-22-09:08: + Import from GitHub must detect remotes for the active project, not the dashboard process fallback. + The remotes API returns an empty list without projectId in multi-project mode, which incorrectly shows "No GitHub remotes detected" for configured repositories. + + FNXC:GitHubImport 2026-06-22-09:22: + Project changes can happen while the modal stays open, so remote discovery must ignore stale responses from earlier projectId requests. + A mounted-only guard is insufficient because the next effect marks the component mounted again before the older request resolves. + */ + fetchGitRemotes(projectId) .then((fetchedRemotes) => { - if (!mountedRef.current) return; + if (cancelled || !mountedRef.current || remoteLoadRequestId !== remoteLoadRequestIdRef.current) return; setRemotes(fetchedRemotes); setLoadingRemotes(false); @@ -179,16 +191,17 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, // If no remotes, owner/repo remain empty }) .catch(() => { - if (mountedRef.current) { + if (!cancelled && mountedRef.current && remoteLoadRequestId === remoteLoadRequestIdRef.current) { setLoadingRemotes(false); } }); return () => { + cancelled = true; mountedRef.current = false; }; } - }, [isOpen]); + }, [isOpen, projectId]); // Handle remote selection change const handleRemoteChange = useCallback((remoteName: string) => { @@ -453,7 +466,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, setImporting(false); } } - }, [activeTab, selectedIssueNumber, selectedPullNumber, owner, repo, onImport, isMobile, mobileView]); + }, [activeTab, selectedIssueNumber, selectedPullNumber, owner, repo, projectId, onImport, isMobile, mobileView]); 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 e86db036ed..3b4b09f2f2 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; +import { act, render, screen, fireEvent, waitFor, within } from "@testing-library/react"; import { GitHubImportModal } from "../GitHubImportModal"; import { apiFetchGitHubIssues, @@ -294,6 +294,64 @@ describe("GitHubImportModal", () => { }); describe("with single remote", () => { + it("loads remotes using the active project id", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + render(); + + await waitFor(() => { + expect(fetchGitRemotes).toHaveBeenCalledWith("project-1"); + }); + }); + + it("ignores stale remote responses after the active project changes", async () => { + const projectARemote: GitRemote[] = [ + { name: "origin", owner: "project-a", repo: "old-repo", url: "https://github.com/project-a/old-repo.git" }, + ]; + const projectBRemote: GitRemote[] = [ + { name: "origin", owner: "project-b", repo: "new-repo", url: "https://github.com/project-b/new-repo.git" }, + ]; + let resolveProjectA!: (value: GitRemote[]) => void; + let resolveProjectB!: (value: GitRemote[]) => void; + vi.mocked(fetchGitRemotes) + .mockImplementationOnce(() => new Promise((resolve) => { + resolveProjectA = resolve; + })) + .mockImplementationOnce(() => new Promise((resolve) => { + resolveProjectB = resolve; + })); + + const { rerender } = render( + , + ); + + await waitFor(() => { + expect(fetchGitRemotes).toHaveBeenCalledWith("project-a"); + }); + + rerender(); + + await waitFor(() => { + expect(fetchGitRemotes).toHaveBeenCalledWith("project-b"); + }); + + await act(async () => { + resolveProjectB(projectBRemote); + }); + + await waitFor(() => { + expect(screen.getByText("project-b/new-repo")).toBeTruthy(); + }); + + await act(async () => { + resolveProjectA(projectARemote); + }); + + await waitFor(() => { + expect(screen.getByText("project-b/new-repo")).toBeTruthy(); + expect(screen.queryByText("project-a/old-repo")).toBeNull(); + }); + }); + it("auto-selects the remote and shows compact pill", async () => { vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); render(); From 5e0e9c08f361d5ea6dcdbbdcbf57a02684d034e9 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 09:36:05 -0700 Subject: [PATCH 2/2] fix(FN-1721): expose ViewHeader to dashboard plugin build Map the shared dashboard ViewHeader module in Vite, Vitest, and app TS paths so bundled plugin dashboard views resolve the host component during PR build/typecheck. References: https://github.com/Runfusion/Fusion/issues/1721 --- packages/dashboard/tsconfig.app.json | 1 + packages/dashboard/tsconfig.test-check.json | 1 + packages/dashboard/vite.config.ts | 1 + packages/dashboard/vitest.config.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/dashboard/tsconfig.app.json b/packages/dashboard/tsconfig.app.json index daf938a315..67a6312c05 100644 --- a/packages/dashboard/tsconfig.app.json +++ b/packages/dashboard/tsconfig.app.json @@ -10,6 +10,7 @@ "paths": { "node-pty": ["./src/types/node-pty/index.d.ts"], "@fusion/dashboard/app/components/TaskCard": ["./app/components/TaskCard.tsx"], + "@fusion/dashboard/app/components/ViewHeader": ["./app/components/ViewHeader.tsx"], "@fusion/dashboard/app/plugins/types": ["./app/plugins/types.ts"], "@fusion/dashboard/app/utils/projectStorage": ["./app/utils/projectStorage.ts"], "@fusion/dashboard/app/utils/taskStuck": ["./app/utils/taskStuck.ts"] diff --git a/packages/dashboard/tsconfig.test-check.json b/packages/dashboard/tsconfig.test-check.json index 8cecf3ac3f..dabb21b736 100644 --- a/packages/dashboard/tsconfig.test-check.json +++ b/packages/dashboard/tsconfig.test-check.json @@ -11,6 +11,7 @@ "@fusion/test-utils": ["../core/src/__test-utils__/workspace.ts"], "node-pty": ["./src/types/node-pty/index.d.ts"], "@fusion/dashboard/app/components/TaskCard": ["./app/components/TaskCard.tsx"], + "@fusion/dashboard/app/components/ViewHeader": ["./app/components/ViewHeader.tsx"], "@fusion/dashboard/app/plugins/types": ["./app/plugins/types.ts"], "@fusion/dashboard/app/utils/projectStorage": ["./app/utils/projectStorage.ts"], "@fusion/dashboard/app/utils/taskStuck": ["./app/utils/taskStuck.ts"] diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts index d49bde4c15..da06e9a570 100644 --- a/packages/dashboard/vite.config.ts +++ b/packages/dashboard/vite.config.ts @@ -125,6 +125,7 @@ export default defineConfig({ alias: { "@fusion/core": resolve(__dirname, "../core/src/types.ts"), "@fusion/dashboard/app/components/TaskCard": resolve(__dirname, "app/components/TaskCard.tsx"), + "@fusion/dashboard/app/components/ViewHeader": resolve(__dirname, "app/components/ViewHeader.tsx"), "@fusion/dashboard/app/plugins/types": resolve(__dirname, "app/plugins/types.ts"), "@fusion/dashboard/app/utils/projectStorage": resolve(__dirname, "app/utils/projectStorage.ts"), "@fusion/dashboard/app/utils/taskStuck": resolve(__dirname, "app/utils/taskStuck.ts"), diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index 19a21eb518..c961da3757 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -424,6 +424,7 @@ export default defineConfig({ "@fusion/plugin-sdk": resolve(__dirname, "../plugin-sdk/src/index.ts"), "@fusion/test-utils": resolve(__dirname, "../core/src/__test-utils__/workspace.ts"), "@fusion/dashboard/app/components/TaskCard": resolve(__dirname, "app/components/TaskCard.tsx"), + "@fusion/dashboard/app/components/ViewHeader": resolve(__dirname, "app/components/ViewHeader.tsx"), "@fusion/dashboard/app/plugins/types": resolve(__dirname, "app/plugins/types.ts"), "@fusion/dashboard/app/utils/projectStorage": resolve(__dirname, "app/utils/projectStorage.ts"), "@fusion/dashboard/app/utils/taskStuck": resolve(__dirname, "app/utils/taskStuck.ts"),