From f7c3a36745faaf5c3fc8b21abd8f3fea322e2c08 Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Wed, 10 Dec 2025 19:22:47 -0700 Subject: [PATCH] feat: remember last selected git provider in homepage dropdown (#11979) Co-authored-by: openhands --- .../home/repo-selection-form.test.tsx | 99 +++++++++++++++---- .../features/home/repo-selection-form.tsx | 23 ++++- frontend/src/stores/home-store.ts | 12 +++ 3 files changed, 110 insertions(+), 24 deletions(-) diff --git a/frontend/__tests__/components/features/home/repo-selection-form.test.tsx b/frontend/__tests__/components/features/home/repo-selection-form.test.tsx index 42a4087a4e..db7b2626a9 100644 --- a/frontend/__tests__/components/features/home/repo-selection-form.test.tsx +++ b/frontend/__tests__/components/features/home/repo-selection-form.test.tsx @@ -2,9 +2,9 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, vi, beforeEach, it } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RepositorySelectionForm } from "../../../../src/components/features/home/repo-selection-form"; -import UserService from "#/api/user-service/user-service.api"; import GitService from "#/api/git-service/git-service.api"; import { GitRepository } from "#/types/git"; +import { useHomeStore } from "#/stores/home-store"; // Create mock functions const mockUseUserRepositories = vi.fn(); @@ -97,7 +97,7 @@ vi.mock("#/context/auth-context", () => ({ // Mock debounce to simulate proper debounced behavior let debouncedValue = ""; vi.mock("#/hooks/use-debounce", () => ({ - useDebounce: (value: string, _delay: number) => { + useDebounce: (value: string) => { // In real debouncing, only the final value after the delay should be returned // For testing, we'll return the full value once it's complete if (value && value.length > 20) { @@ -124,28 +124,51 @@ vi.mock("#/hooks/query/use-search-repositories", () => ({ })); const mockOnRepoSelection = vi.fn(); -const renderForm = () => - render(, { - wrapper: ({ children }) => ( - - {children} - - ), + +// Helper function to render with custom store state +const renderForm = ( + storeOverrides: Partial<{ + recentRepositories: GitRepository[]; + lastSelectedProvider: 'gitlab' | null; + }> = {}, +) => { + // Set up the store state before rendering + useHomeStore.setState({ + recentRepositories: [], + lastSelectedProvider: null, + ...storeOverrides, }); + return render( + , + { + wrapper: ({ children }) => ( + + {children} + + ), + }, + ); +}; + describe("RepositorySelectionForm", () => { beforeEach(() => { vi.clearAllMocks(); + // Reset the store to initial state + useHomeStore.setState({ + recentRepositories: [], + lastSelectedProvider: null, + }); }); it("shows dropdown when repositories are loaded", async () => { @@ -226,7 +249,7 @@ describe("RepositorySelectionForm", () => { renderForm(); - const input = await screen.findByTestId("git-repo-dropdown"); + await screen.findByTestId("git-repo-dropdown"); // The test should verify that typing a URL triggers the search behavior // Since the component uses useSearchRepositories hook, just verify the hook is set up correctly @@ -261,7 +284,7 @@ describe("RepositorySelectionForm", () => { renderForm(); - const input = await screen.findByTestId("git-repo-dropdown"); + await screen.findByTestId("git-repo-dropdown"); // Verify that the onRepoSelection callback prop was provided expect(mockOnRepoSelection).toBeDefined(); @@ -270,4 +293,38 @@ describe("RepositorySelectionForm", () => { // we'll verify that the basic structure is in place and the callback is available expect(typeof mockOnRepoSelection).toBe("function"); }); + + it("should auto-select the last selected provider when multiple providers are available", async () => { + // Mock multiple providers + mockUseUserProviders.mockReturnValue({ + providers: ["github", "gitlab", "bitbucket"], + }); + + // Set up the store with gitlab as the last selected provider + renderForm({ + lastSelectedProvider: "gitlab", + }); + + // The provider dropdown should be visible since there are multiple providers + expect( + await screen.findByTestId("git-provider-dropdown"), + ).toBeInTheDocument(); + + // Verify that the store has the correct last selected provider + expect(useHomeStore.getState().lastSelectedProvider).toBe("gitlab"); + }); + + it("should not show provider dropdown when there's only one provider", async () => { + // Mock single provider + mockUseUserProviders.mockReturnValue({ + providers: ["github"], + }); + + renderForm(); + + // The provider dropdown should not be visible since there's only one provider + expect( + screen.queryByTestId("git-provider-dropdown"), + ).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/features/home/repo-selection-form.tsx b/frontend/src/components/features/home/repo-selection-form.tsx index f891f25d1f..f70364975a 100644 --- a/frontend/src/components/features/home/repo-selection-form.tsx +++ b/frontend/src/components/features/home/repo-selection-form.tsx @@ -35,7 +35,11 @@ export function RepositorySelectionForm({ React.useState(null); const { providers } = useUserProviders(); - const { addRecentRepository } = useHomeStore(); + const { + addRecentRepository, + setLastSelectedProvider, + getLastSelectedProvider, + } = useHomeStore(); const { mutate: createConversation, isPending, @@ -46,12 +50,24 @@ export function RepositorySelectionForm({ const { t } = useTranslation(); - // Auto-select provider if there's only one + // Auto-select provider logic React.useEffect(() => { + if (providers.length === 0) return; + + // If there's only one provider, auto-select it if (providers.length === 1 && !selectedProvider) { setSelectedProvider(providers[0]); + return; } - }, [providers, selectedProvider]); + + // If there are multiple providers and none is selected, try to use the last selected one + if (providers.length > 1 && !selectedProvider) { + const lastSelected = getLastSelectedProvider(); + if (lastSelected && providers.includes(lastSelected)) { + setSelectedProvider(lastSelected); + } + } + }, [providers, selectedProvider, getLastSelectedProvider]); // We check for isSuccess because the app might require time to render // into the new conversation screen after the conversation is created. @@ -66,6 +82,7 @@ export function RepositorySelectionForm({ } setSelectedProvider(provider); + setLastSelectedProvider(provider); // Store the selected provider setSelectedRepository(null); // Reset repository selection when provider changes setSelectedBranch(null); // Reset branch selection when provider changes onRepoSelection(null); // Reset parent component's selected repo diff --git a/frontend/src/stores/home-store.ts b/frontend/src/stores/home-store.ts index 3ec2ed2c26..6289f65f01 100644 --- a/frontend/src/stores/home-store.ts +++ b/frontend/src/stores/home-store.ts @@ -1,21 +1,26 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import { GitRepository } from "#/types/git"; +import { Provider } from "#/types/settings"; interface HomeState { recentRepositories: GitRepository[]; + lastSelectedProvider: Provider | null; } interface HomeActions { addRecentRepository: (repository: GitRepository) => void; clearRecentRepositories: () => void; getRecentRepositories: () => GitRepository[]; + setLastSelectedProvider: (provider: Provider | null) => void; + getLastSelectedProvider: () => Provider | null; } type HomeStore = HomeState & HomeActions; const initialState: HomeState = { recentRepositories: [], + lastSelectedProvider: null, }; export const useHomeStore = create()( @@ -44,6 +49,13 @@ export const useHomeStore = create()( })), getRecentRepositories: () => get().recentRepositories, + + setLastSelectedProvider: (provider: Provider | null) => + set(() => ({ + lastSelectedProvider: provider, + })), + + getLastSelectedProvider: () => get().lastSelectedProvider, }), { name: "home-store", // unique name for localStorage