refactor(frontend): useUserRepositories hook (#8207)

This commit is contained in:
sp.wack 2025-05-01 23:01:02 +04:00 committed by GitHub
parent fc4ad2f8c3
commit d36cde5060
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 56 additions and 190 deletions

View File

@ -8,7 +8,6 @@ import { createRoutesStub, Outlet } from "react-router";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { GitRepository } from "#/types/git";
import * as GitService from "#/api/git";
import { RepoConnector } from "#/components/features/home/repo-connector";
const renderRepoConnector = (initialProvidersAreSet = true) => {
@ -74,13 +73,10 @@ describe("RepoConnector", () => {
it("should render the available repositories in the dropdown", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderRepoConnector();
@ -96,13 +92,10 @@ describe("RepoConnector", () => {
it("should only enable the launch button if a repo is selected", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderRepoConnector();
@ -145,13 +138,10 @@ describe("RepoConnector", () => {
it("should create a conversation and redirect with the selected repo when pressing the launch button", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderRepoConnector();
@ -190,13 +180,10 @@ describe("RepoConnector", () => {
it("should change the launch button text to 'Loading...' when creating a conversation", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderRepoConnector();

View File

@ -9,7 +9,6 @@ import { SuggestedTask } from "#/components/features/home/tasks/task.types";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { TaskCard } from "#/components/features/home/tasks/task-card";
import * as GitService from "#/api/git";
import { GitRepository } from "#/types/git";
const MOCK_TASK_1: SuggestedTask = {
@ -20,30 +19,6 @@ const MOCK_TASK_1: SuggestedTask = {
git_provider: "github",
};
const MOCK_TASK_2: SuggestedTask = {
issue_number: 456,
repo: "repo2",
title: "Task 2",
task_type: "FAILING_CHECKS",
git_provider: "github",
};
const MOCK_TASK_3: SuggestedTask = {
issue_number: 789,
repo: "repo3",
title: "Task 3",
task_type: "UNRESOLVED_COMMENTS",
git_provider: "gitlab",
};
const MOCK_TASK_4: SuggestedTask = {
issue_number: 101112,
repo: "repo4",
title: "Task 4",
task_type: "OPEN_ISSUE",
git_provider: "gitlab",
};
const MOCK_RESPOSITORIES: GitRepository[] = [
{ id: 1, full_name: "repo1", git_provider: "github", is_public: true },
{ id: 2, full_name: "repo2", git_provider: "github", is_public: true },
@ -98,13 +73,10 @@ describe("TaskCard", () => {
describe("creating suggested task conversation", () => {
beforeEach(() => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
});
it("should call create conversation with suggest task trigger and selected suggested task", async () => {

View File

@ -8,7 +8,6 @@ import { setupStore } from "test-utils";
import { AxiosError } from "axios";
import HomeScreen from "#/routes/home";
import { AuthProvider } from "#/context/auth-context";
import * as GitService from "#/api/git";
import { GitRepository } from "#/types/git";
import OpenHands from "#/api/open-hands";
import MainApp from "#/routes/root-layout";
@ -102,13 +101,10 @@ describe("HomeScreen", () => {
it("should filter the suggested tasks based on the selected repository", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderHomeScreen();
@ -140,13 +136,10 @@ describe("HomeScreen", () => {
it("should reset the filtered tasks when the selected repository is cleared", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderHomeScreen();
@ -218,13 +211,10 @@ describe("HomeScreen", () => {
beforeEach(() => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
GitService,
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
});
it("should disable the other launch buttons when the header launch button is clicked", async () => {

View File

@ -1,75 +0,0 @@
import { GitRepository } from "#/types/git";
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { openHands } from "./open-hands-axios";
/**
* Retrieves repositories where OpenHands Github App has been installed
* @param installationIndex Pagination cursor position for app installation IDs
* @param installations Collection of all App installation IDs for OpenHands Github App
* @returns A list of repositories
*/
export const retrieveGitHubAppRepositories = async (
installationIndex: number,
installations: number[],
page = 1,
per_page = 30,
) => {
const installationId = installations[installationIndex];
const response = await openHands.get<GitRepository[]>(
"/api/user/repositories",
{
params: {
sort: "pushed",
page,
per_page,
installation_id: installationId,
},
},
);
const link =
response.data.length > 0 && response.data[0].link_header
? response.data[0].link_header
: "";
const nextPage = extractNextPageFromLink(link);
let nextInstallation: number | null;
if (nextPage) {
nextInstallation = installationIndex;
} else if (installationIndex + 1 < installations.length) {
nextInstallation = installationIndex + 1;
} else {
nextInstallation = null;
}
return {
data: response.data,
nextPage,
installationIndex: nextInstallation,
};
};
/**
* Given a PAT, retrieves the repositories of the user
* @returns A list of repositories
*/
export const retrieveUserGitRepositories = async () => {
const response = await openHands.get<GitRepository[]>(
"/api/user/repositories",
{
params: {
sort: "pushed",
},
},
);
// Check if any provider has more results
const link =
response.data.length > 0 && response.data[0].link_header
? response.data[0].link_header
: "";
const nextPage = extractNextPageFromLink(link);
return { data: response.data, nextPage };
};

View File

@ -297,6 +297,23 @@ class OpenHands {
);
return data;
}
/**
* Given a PAT, retrieves the repositories of the user
* @returns A list of repositories
*/
static async retrieveUserGitRepositories() {
const { data } = await openHands.get<GitRepository[]>(
"/api/user/repositories",
{
params: {
sort: "pushed",
},
},
);
return data;
}
}
export default OpenHands;

View File

@ -37,9 +37,6 @@ export function GitRepositoriesSuggestionBox({
const isLoading = isUserReposLoading || isSearchReposLoading;
const repositories =
userRepositories?.pages.flatMap((page) => page.data) || [];
const handleConnectToGitHub = () => {
if (gitHubAuthUrl) {
window.location.href = gitHubAuthUrl;
@ -59,7 +56,7 @@ export function GitRepositoriesSuggestionBox({
onInputChange={setSearchQuery}
onSelect={handleSubmit}
publicRepositories={searchedRepos || []}
userRepositories={repositories}
userRepositories={userRepositories || []}
isLoading={isLoading}
/>
) : (

View File

@ -11,7 +11,7 @@ const mockUseAuth = vi.fn();
// Setup default mock returns
mockUseUserRepositories.mockReturnValue({
data: { pages: [{ data: [] }] },
data: [],
isLoading: false,
isError: false,
});
@ -88,26 +88,20 @@ describe("RepositorySelectionForm", () => {
test("shows dropdown when repositories are loaded", () => {
// Setup loaded repositories
mockUseUserRepositories.mockReturnValue({
data: {
pages: [
{
data: [
{
id: 1,
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: 2,
full_name: "user/repo2",
git_provider: "github",
is_public: true,
},
],
},
],
},
data: [
{
id: 1,
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: 2,
full_name: "user/repo2",
git_provider: "github",
is_public: true,
},
],
isLoading: false,
isError: false,
});

View File

@ -87,14 +87,13 @@ export function RepositorySelectionForm({
const isCreatingConversation =
isPending || isSuccess || isCreatingConversationElsewhere;
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
const repositoriesItems = repositoriesList?.map((repo) => ({
const repositoriesItems = repositories?.map((repo) => ({
key: repo.id,
label: repo.full_name,
}));
const handleRepoSelection = (key: React.Key | null) => {
const selectedRepo = repositoriesList?.find(
const selectedRepo = repositories?.find(
(repo) => repo.id.toString() === key,
);

View File

@ -27,8 +27,7 @@ export function TaskCard({ task }: TaskCardProps) {
const { t } = useTranslation();
const getRepo = (repo: string, git_provider: Provider) => {
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
const selectedRepo = repositoriesList?.find(
const selectedRepo = repositories?.find(
(repository) =>
repository.full_name === repo &&
repository.git_provider === git_provider,

View File

@ -1,29 +1,15 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import React from "react";
import { retrieveUserGitRepositories } from "#/api/git";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "#/context/auth-context";
import OpenHands from "#/api/open-hands";
export const useUserRepositories = () => {
const { providerTokensSet, providersAreSet } = useAuth();
const repos = useInfiniteQuery({
return useQuery({
queryKey: ["repositories", providerTokensSet],
queryFn: async () => retrieveUserGitRepositories(),
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
queryFn: OpenHands.retrieveUserGitRepositories,
enabled: providersAreSet,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
// (nextui autocomplete doesn't support onEndReached nor is it compatible for extending)
const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos;
React.useEffect(() => {
if (!isFetchingNextPage && isSuccess && hasNextPage) {
fetchNextPage();
}
}, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]);
return repos;
};