Retain eligible providers during refresh
apps/web/hooks/domains/integrations/use-remote-repositories.ts ↗Keeps current providers visible while refresh loads and filters only providers that are no longer eligible.
Diff: keep eligible providers
workspaceId: string,
refreshVersion: number,
eligibility: BuiltInRepositoryEligibility,
): RepositorySourceState {
const [repos, setRepos] = useState<RemoteRepository[]>([]);
const [loading, setLoading] = useState(true);
const [sourceErrors, setSourceErrors] = useState<RemoteRepositorySourceError[]>([]);
const [availableProviders, setAvailableProviders] = useState<RemoteRepositoryProvider[]>([]);
const workspaceRef = useRef(workspaceId);
useEffect(() => {
let cancelled = false;
const sameWorkspace = workspaceRef.current === workspaceId;
workspaceRef.current = workspaceId;
setRepos([]);
setAvailableProviders([]);
setAvailableProviders((current) =>
sameWorkspace ? current.filter((provider) => eligibility.providers.has(provider)) : [],
);
setSourceErrors([]);
setLoading(true);
Guard stale responses
loadBuiltInRepositories(workspaceId, eligibility.providers)
.then((result) => {
if (cancelled) return;
setRepos(result.repos);
setAvailableProviders(result.availableProviders);
setSourceErrors(result.sourceErrors);
})
.catch((cause) => {
if (!cancelled) setSourceErrors([{ provider: "built-in", error: toError(cause) }]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
Refresh re-evaluates eligibility
apps/web/hooks/domains/integrations/use-remote-repositories.ts ↗Refresh triggers provider status checks and bumps the version so the effect reloads with current eligibility.
Eligibility and refresh
function useBuiltInRepositoryAccess(workspaceId: string): BuiltInRepositoryAccess {
const githubStatus = useGitHubStatus(workspaceId);
const gitlabStatus = useGitLabStatus(workspaceId);
const azureDevOpsConnection = useAzureDevOpsConnection(workspaceId || undefined);
const providers = useMemo(() => {
const eligible = new Set<RemoteRepositoryProvider>();
if (hasProviderConnection(githubStatus.status)) eligible.add("github");
if (hasProviderConnection(gitlabStatus.status)) eligible.add("gitlab");
if (azureDevOpsConnection.data?.hasSecret && azureDevOpsConnection.data.lastOk) {
eligible.add("azure_devops");
}
return eligible;
}, [azureDevOpsConnection.data, githubStatus.status, gitlabStatus.status]);
const refresh = useCallback(() => {
void githubStatus.refresh();
void gitlabStatus.refresh();
azureDevOpsConnection.refresh();
}, [azureDevOpsConnection.refresh, githubStatus.refresh, gitlabStatus.refresh]);
return { eligibility: { providers, loading }, refresh };
}
Hook refresh
const { eligibility, refresh: refreshBuiltIns } = useBuiltInRepositoryAccess(workspaceId);
const builtInSource = useBuiltInRepositorySource(workspaceId, refreshVersion, eligibility);
const refresh = useCallback(() => {
refreshBuiltIns();
setRefreshVersion((version) => version + 1);
}, [refreshBuiltIns]);
Tests for refresh and stale guard
apps/web/hooks/domains/integrations/use-remote-repositories.test.tsx ↗Tests prove refresh keeps eligible providers, clears errors while loading, and ignores stale workspace results.
Keep providers while loading
it("keeps currently eligible providers available while a refresh is loading", async () => {
mocks.fetchAccessibleRepos.mockResolvedValue([]);
mocks.listUserProjects.mockResolvedValue({ projects: [] });
mocks.listAzureDevOpsProjects.mockResolvedValue({ projects: [] });
const { result } = renderHook(() => useRemoteRepositories(WORKSPACE_ID));
await waitFor(() => expect(result.current.loading).toBe(false));
let resolveRefresh: ((repos: never[]) => void) | undefined;
mocks.fetchAccessibleRepos.mockImplementationOnce(
() => new Promise((resolve) => (resolveRefresh = resolve)),
);
setBuiltInAvailability({ azureDevOps: false, gitlab: false });
act(() => result.current.refresh?.());
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
expect(result.current.availableProviders).toEqual(["github"]);
act(() => resolveRefresh?.([]));
await waitFor(() => expect(result.current.loading).toBe(false));
});
Stale workspace guard
it("does not publish a stale repository result after a workspace change", async () => {
setBuiltInAvailability({ gitlab: false, azureDevOps: false });
let resolveFirst: ((value: unknown) => void) | undefined;
let resolveSecond: ((value: unknown) => void) | undefined;
mocks.fetchAccessibleRepos
.mockImplementationOnce(() => new Promise((resolve) => (resolveFirst = resolve)))
.mockImplementationOnce(() => new Promise((resolve) => (resolveSecond = resolve)));
const { result, rerender } = renderHook(
({ workspaceId }) => useRemoteRepositories(workspaceId),
{ initialProps: { workspaceId: WORKSPACE_ID } },
);
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(1));
rerender({ workspaceId: "workspace-2" });
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
await act(async () => {
resolveFirst?.([{ owner: "acme", name: "stale", full_name: "acme/stale", default_branch: "main", private: false }]);
});
expect(result.current.repos).toEqual([]);
await act(async () => {
resolveSecond?.([{ owner: "acme", name: "current", full_name: "acme/current", default_branch: "main", private: false }]);
});
await waitFor(() => expect(result.current.repos.map((r) => r.fullName)).toEqual(["acme/current"]));
});