feat: remember last selected git provider in homepage dropdown (#11979)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell 2025-12-10 19:22:47 -07:00 committed by GitHub
parent a593730b21
commit f7c3a36745
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 24 deletions

View File

@ -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(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />, {
wrapper: ({ children }) => (
<QueryClientProvider
client={
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
}
>
{children}
</QueryClientProvider>
),
// 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(
<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />,
{
wrapper: ({ children }) => (
<QueryClientProvider
client={
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
}
>
{children}
</QueryClientProvider>
),
},
);
};
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();
});
});

View File

@ -35,7 +35,11 @@ export function RepositorySelectionForm({
React.useState<Provider | null>(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

View File

@ -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<HomeStore>()(
@ -44,6 +49,13 @@ export const useHomeStore = create<HomeStore>()(
})),
getRecentRepositories: () => get().recentRepositories,
setLastSelectedProvider: (provider: Provider | null) =>
set(() => ({
lastSelectedProvider: provider,
})),
getLastSelectedProvider: () => get().lastSelectedProvider,
}),
{
name: "home-store", // unique name for localStorage