mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 07:18:10 -05:00
Add loading indicator to repository dropdown on home page (#8015)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -74,7 +74,8 @@ describe("RepoConnector", () => {
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const dropdown = screen.getByTestId("repo-dropdown");
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -98,7 +99,8 @@ describe("RepoConnector", () => {
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
expect(launchButton).toBeDisabled();
|
||||
|
||||
const dropdown = screen.getByTestId("repo-dropdown");
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
await userEvent.click(screen.getByText("rbren/polaris"));
|
||||
|
||||
@@ -132,6 +134,14 @@ 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,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -144,7 +154,9 @@ describe("RepoConnector", () => {
|
||||
expect(createConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
// select a repository from the dropdown
|
||||
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
|
||||
const dropdown = await waitFor(() =>
|
||||
within(repoConnector).getByTestId("repo-dropdown")
|
||||
);
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
const repoOption = screen.getByText("rbren/polaris");
|
||||
@@ -178,7 +190,8 @@ describe("RepoConnector", () => {
|
||||
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
|
||||
const dropdown = screen.getByTestId("repo-dropdown");
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
await userEvent.click(screen.getByText("rbren/polaris"));
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||
import { RepositorySelectionForm } from "./repo-selection-form";
|
||||
|
||||
// Create mock functions
|
||||
const mockUseUserRepositories = vi.fn();
|
||||
const mockUseCreateConversation = vi.fn();
|
||||
const mockUseIsCreatingConversation = vi.fn();
|
||||
const mockUseTranslation = vi.fn();
|
||||
const mockUseAuth = vi.fn();
|
||||
|
||||
// Setup default mock returns
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: { pages: [{ data: [] }] },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseCreateConversation.mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
});
|
||||
|
||||
mockUseIsCreatingConversation.mockReturnValue(false);
|
||||
|
||||
mockUseTranslation.mockReturnValue({ t: (key: string) => key });
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
providersAreSet: true,
|
||||
user: {
|
||||
id: 1,
|
||||
login: "testuser",
|
||||
avatar_url: "https://example.com/avatar.png",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
company: "Test Company",
|
||||
},
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock the modules
|
||||
vi.mock("#/hooks/query/use-user-repositories", () => ({
|
||||
useUserRepositories: () => mockUseUserRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/mutation/use-create-conversation", () => ({
|
||||
useCreateConversation: () => mockUseCreateConversation(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-is-creating-conversation", () => ({
|
||||
useIsCreatingConversation: () => mockUseIsCreatingConversation(),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => mockUseTranslation(),
|
||||
}));
|
||||
|
||||
vi.mock("#/context/auth-context", () => ({
|
||||
useAuth: () => mockUseAuth(),
|
||||
}));
|
||||
|
||||
describe("RepositorySelectionForm", () => {
|
||||
const mockOnRepoSelection = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("shows loading indicator when repositories are being fetched", () => {
|
||||
// Setup loading state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if loading indicator is displayed
|
||||
expect(screen.getByTestId("repo-dropdown-loading")).toBeInTheDocument();
|
||||
expect(screen.getByText("HOME$LOADING_REPOSITORIES")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if dropdown is displayed
|
||||
expect(screen.getByTestId("repo-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows error message when repository fetch fails", () => {
|
||||
// Setup error state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: new Error("Failed to fetch repositories"),
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if error message is displayed
|
||||
expect(screen.getByTestId("repo-dropdown-error")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("HOME$FAILED_TO_LOAD_REPOSITORIES"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
|
||||
@@ -11,12 +12,68 @@ interface RepositorySelectionFormProps {
|
||||
onRepoSelection: (repoTitle: string | null) => void;
|
||||
}
|
||||
|
||||
// Loading state component
|
||||
function RepositoryLoadingState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state component
|
||||
function RepositoryErrorState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Repository dropdown component
|
||||
interface RepositoryDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
onSelectionChange: (key: React.Key | null) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function RepositoryDropdown({
|
||||
items,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
}: RepositoryDropdownProps) {
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RepositorySelectionForm({
|
||||
onRepoSelection,
|
||||
}: RepositorySelectionFormProps) {
|
||||
const [selectedRepository, setSelectedRepository] =
|
||||
React.useState<GitRepository | null>(null);
|
||||
const { data: repositories } = useUserRepositories();
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
isError: isRepositoriesError,
|
||||
} = useUserRepositories();
|
||||
const {
|
||||
mutate: createConversation,
|
||||
isPending,
|
||||
@@ -52,23 +109,39 @@ export function RepositorySelectionForm({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
// Render the appropriate UI based on the loading/error state
|
||||
const renderRepositorySelector = () => {
|
||||
if (isLoadingRepositories) {
|
||||
return <RepositoryLoadingState />;
|
||||
}
|
||||
|
||||
if (isRepositoriesError) {
|
||||
return <RepositoryErrorState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RepositoryDropdown
|
||||
items={repositoriesItems || []}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={handleRepoSelection}
|
||||
onInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderRepositorySelector()}
|
||||
|
||||
<BrandButton
|
||||
testId="repo-launch-button"
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={!selectedRepository || isCreatingConversation}
|
||||
isDisabled={
|
||||
!selectedRepository ||
|
||||
isCreatingConversation ||
|
||||
isLoadingRepositories ||
|
||||
isRepositoriesError
|
||||
}
|
||||
onClick={() => createConversation({ selectedRepository })}
|
||||
>
|
||||
{!isCreatingConversation && "Launch"}
|
||||
|
||||
@@ -6,6 +6,8 @@ export enum I18nKey {
|
||||
HOME$NOT_SURE_HOW_TO_START = "HOME$NOT_SURE_HOW_TO_START",
|
||||
HOME$CONNECT_TO_REPOSITORY = "HOME$CONNECT_TO_REPOSITORY",
|
||||
HOME$LOADING = "HOME$LOADING",
|
||||
HOME$LOADING_REPOSITORIES = "HOME$LOADING_REPOSITORIES",
|
||||
HOME$FAILED_TO_LOAD_REPOSITORIES = "HOME$FAILED_TO_LOAD_REPOSITORIES",
|
||||
HOME$OPEN_ISSUE = "HOME$OPEN_ISSUE",
|
||||
HOME$FIX_FAILING_CHECKS = "HOME$FIX_FAILING_CHECKS",
|
||||
HOME$RESOLVE_MERGE_CONFLICTS = "HOME$RESOLVE_MERGE_CONFLICTS",
|
||||
|
||||
@@ -89,6 +89,36 @@
|
||||
"tr": "Yükleniyor...",
|
||||
"de": "Wird geladen..."
|
||||
},
|
||||
"HOME$LOADING_REPOSITORIES": {
|
||||
"en": "Loading repositories...",
|
||||
"ja": "リポジトリを読み込み中...",
|
||||
"zh-CN": "加载仓库中...",
|
||||
"zh-TW": "載入儲存庫中...",
|
||||
"ko-KR": "저장소 로딩 중...",
|
||||
"no": "Laster repositories...",
|
||||
"it": "Caricamento repository in corso...",
|
||||
"pt": "Carregando repositórios...",
|
||||
"es": "Cargando repositorios...",
|
||||
"ar": "جار تحميل المستودعات...",
|
||||
"fr": "Chargement des dépôts...",
|
||||
"tr": "Depolar yükleniyor...",
|
||||
"de": "Repositories werden geladen..."
|
||||
},
|
||||
"HOME$FAILED_TO_LOAD_REPOSITORIES": {
|
||||
"en": "Failed to load repositories",
|
||||
"ja": "リポジトリの読み込みに失敗しました",
|
||||
"zh-CN": "加载仓库失败",
|
||||
"zh-TW": "載入儲存庫失敗",
|
||||
"ko-KR": "저장소 로딩 실패",
|
||||
"no": "Kunne ikke laste repositories",
|
||||
"it": "Impossibile caricare i repository",
|
||||
"pt": "Falha ao carregar repositórios",
|
||||
"es": "Error al cargar repositorios",
|
||||
"ar": "فشل في تحميل المستودعات",
|
||||
"fr": "Échec du chargement des dépôts",
|
||||
"tr": "Depolar yüklenemedi",
|
||||
"de": "Fehler beim Laden der Repositories"
|
||||
},
|
||||
"HOME$OPEN_ISSUE": {
|
||||
"en": "Open issue",
|
||||
"ja": "オープンな課題",
|
||||
|
||||
Reference in New Issue
Block a user