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
This commit is contained in:
5
.changeset/github-import-project-remotes.md
Normal file
5
.changeset/github-import-project-remotes.md
Normal file
@@ -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.
|
||||
@@ -86,6 +86,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
const [loadingRemotes, setLoadingRemotes] = useState(false);
|
||||
const [selectedRemoteName, setSelectedRemoteName] = useState<string>("");
|
||||
const mountedRef = useRef(false);
|
||||
const remoteLoadRequestIdRef = useRef(0);
|
||||
const modalRef = useRef<HTMLDivElement>(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);
|
||||
|
||||
@@ -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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} projectId="project-1" />);
|
||||
|
||||
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(
|
||||
<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} projectId="project-a" />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchGitRemotes).toHaveBeenCalledWith("project-a");
|
||||
});
|
||||
|
||||
rerender(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} projectId="project-b" />);
|
||||
|
||||
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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
|
||||
|
||||
Reference in New Issue
Block a user