From e749d7963a0ba6d26d6080968509c447a653fa30 Mon Sep 17 00:00:00 2001 From: "@tomny-dev" <20028678+tomny-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:16:29 +0000 Subject: [PATCH 1/3] feat: search repositories during environment setup --- .../CreateEnvironmentPage.client.test.tsx | 3 + ...ironmentRepositorySelector.client.test.tsx | 125 ++++++++++++++++++ .../EnvironmentRepositorySelector.tsx | 75 ++++++++--- .../src/components/system/primitives/icons.ts | 1 + 4 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx diff --git a/apps/web/src/components/settings/environments/CreateEnvironmentPage.client.test.tsx b/apps/web/src/components/settings/environments/CreateEnvironmentPage.client.test.tsx index a21e975bb..088e0d947 100644 --- a/apps/web/src/components/settings/environments/CreateEnvironmentPage.client.test.tsx +++ b/apps/web/src/components/settings/environments/CreateEnvironmentPage.client.test.tsx @@ -263,8 +263,11 @@ vi.mock('@/components/system', () => ({ ), HandMetal: (props: SVGProps) => , Info: (props: SVGProps) => , + Input: (props: InputHTMLAttributes) => , Loader2: (props: SVGProps) => , + PackagePlus: (props: SVGProps) => , Plus: (props: SVGProps) => , + Search: (props: SVGProps) => , ScrollArea: ({ children, ...props diff --git a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx new file mode 100644 index 000000000..6d0dcf138 --- /dev/null +++ b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import { EnvironmentRepositorySelector } from './EnvironmentRepositorySelector'; + +const repositories = [ + { id: 'repo-web', fullName: 'Acme/web' }, + { id: 'repo-api', fullName: 'acme/api' }, + { id: 'repo-roomote', fullName: 'RoomoteInc/Roomote' }, +]; + +function SelectorHarness({ + initialSelection = [], + onCreateRepository, +}: { + initialSelection?: string[]; + onCreateRepository?: () => void; +}) { + const [selectedRepositoryIds, setSelectedRepositoryIds] = + useState(initialSelection); + + return ( + + setSelectedRepositoryIds((currentSelection) => + currentSelection.includes(repositoryId) + ? currentSelection.filter((id) => id !== repositoryId) + : [...currentSelection, repositoryId], + ) + } + onCreateRepository={onCreateRepository} + inputPrefix="test-repository" + /> + ); +} + +describe('EnvironmentRepositorySelector', () => { + it('filters repository full names case-insensitively and restores sorted results', async () => { + render(); + + expect( + screen.getAllByRole('checkbox').map((checkbox) => checkbox.id), + ).toEqual([ + 'test-repository-repo-api', + 'test-repository-repo-web', + 'test-repository-repo-roomote', + ]); + + const search = screen.getByRole('searchbox', { + name: 'Search repositories', + }); + fireEvent.change(search, { target: { value: 'ROOMOTE' } }); + + await waitFor(() => { + expect(screen.getByText('RoomoteInc/Roomote')).toBeInTheDocument(); + expect(screen.queryByText('acme/api')).not.toBeInTheDocument(); + }); + + fireEvent.change(search, { target: { value: '' } }); + + await waitFor(() => { + expect(screen.getAllByRole('checkbox')).toHaveLength(3); + }); + }); + + it('preserves selected repositories while they are filtered out', async () => { + render(); + + const search = screen.getByRole('searchbox', { + name: 'Search repositories', + }); + fireEvent.change(search, { target: { value: 'api' } }); + + await waitFor(() => { + expect(screen.queryByText('Acme/web')).not.toBeInTheDocument(); + }); + + fireEvent.change(search, { target: { value: '' } }); + + await waitFor(() => { + expect(screen.getByLabelText('Acme/web')).toBeChecked(); + }); + }); + + it('keeps repository creation available when no repositories match', async () => { + const onCreateRepository = vi.fn(); + render(); + + fireEvent.change( + screen.getByRole('searchbox', { name: 'Search repositories' }), + { target: { value: 'missing' } }, + ); + + await waitFor(() => { + expect(screen.getByText('No repositories found.')).toBeInTheDocument(); + }); + + fireEvent.click( + screen.getByRole('button', { name: 'Create a new repository' }), + ); + expect(onCreateRepository).toHaveBeenCalledOnce(); + }); + + it('omits search for an empty list while retaining repository creation', () => { + const onCreateRepository = vi.fn(); + + render( + , + ); + + expect( + screen.queryByRole('searchbox', { name: 'Search repositories' }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Create a new repository' }), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx index 6199d3528..8dc3b6486 100644 --- a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx +++ b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx @@ -1,9 +1,14 @@ 'use client'; -import { useMemo } from 'react'; +import { useDeferredValue, useMemo, useState } from 'react'; -import { Checkbox, ScrollArea } from '@/components/system'; -import { PackagePlus } from 'lucide-react'; +import { + Checkbox, + Input, + PackagePlus, + ScrollArea, + Search, +} from '@/components/system'; type RepositorySummary = { id: string; @@ -25,6 +30,8 @@ export function EnvironmentRepositorySelector({ inputPrefix?: string; heightClassName?: string; }) { + const [search, setSearch] = useState(''); + const deferredSearch = useDeferredValue(search); const sortedRepositories = useMemo( () => [...repositories].sort((left, right) => @@ -32,10 +39,36 @@ export function EnvironmentRepositorySelector({ ), [repositories], ); + const filteredRepositories = useMemo(() => { + const query = deferredSearch.trim().toLowerCase(); + + if (!query) { + return sortedRepositories; + } + + return sortedRepositories.filter((repository) => + repository.fullName.toLowerCase().includes(query), + ); + }, [deferredSearch, sortedRepositories]); return (
+ {repositories.length > 0 ? ( +
+
+ + setSearch(event.currentTarget.value)} + className="pl-9" + /> +
+
+ ) : null} {onCreateRepository ? (

); diff --git a/apps/web/src/components/system/primitives/icons.ts b/apps/web/src/components/system/primitives/icons.ts index c4d10f058..2c1abc164 100644 --- a/apps/web/src/components/system/primitives/icons.ts +++ b/apps/web/src/components/system/primitives/icons.ts @@ -141,6 +141,7 @@ export { Music2Icon, PanelLeftClose, PanelLeftOpen, + PackagePlus, PaperclipIcon, Pencil, Pin, From 8f7cfa22ef29417945ecee75d7944f4f07e3cbe3 Mon Sep 17 00:00:00 2001 From: "@tomny-dev" <20028678+tomny-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:28:11 +0000 Subject: [PATCH 2/3] fix: avoid duplicate repository search in onboarding --- .../setup/StepRepoSelection.client.test.tsx | 1 + .../app/(onboarding)/setup/StepRepoSelection.tsx | 2 ++ ...EnvironmentRepositorySelector.client.test.tsx | 16 ++++++++++++++++ .../EnvironmentRepositorySelector.tsx | 4 +++- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.client.test.tsx b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.client.test.tsx index bb98803b8..4c7737a9c 100644 --- a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.client.test.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.client.test.tsx @@ -273,6 +273,7 @@ vi.mock('@/components/system', () => ({ Input: (props: InputHTMLAttributes) => , ArrowRight: (props: SVGProps) => , Loader2: (props: SVGProps) => , + PackagePlus: (props: SVGProps) => , Plus: (props: SVGProps) => , RefreshCcw: (props: SVGProps) => , RotateCw: (props: SVGProps) => , diff --git a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx index dc3f9fc72..08449b956 100644 --- a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx @@ -359,6 +359,7 @@ export function StepRepoSelection({ selectedRepositoryIds={selectedRepositoryIds} onToggleRepository={toggleRepository} onCreateRepository={() => setCreateRepoDialogOpen(true)} + showSearch={false} inputPrefix="setup-repository" heightClassName="h-auto" /> @@ -532,6 +533,7 @@ export function StepRepoSelection({ selectedRepositoryIds={selectedRepositoryIds} onToggleRepository={toggleRepository} onCreateRepository={() => setCreateRepoDialogOpen(true)} + showSearch={false} inputPrefix="setup-repository" heightClassName="max-h-[calc(var(--effective-viewport-height)-40rem)] md:h-[18.75rem]" /> diff --git a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx index 6d0dcf138..6bfb4fd98 100644 --- a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx +++ b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.client.test.tsx @@ -122,4 +122,20 @@ describe('EnvironmentRepositorySelector', () => { screen.getByRole('button', { name: 'Create a new repository' }), ).toBeInTheDocument(); }); + + it('allows callers with an existing filter to suppress search', () => { + render( + , + ); + + expect( + screen.queryByRole('searchbox', { name: 'Search repositories' }), + ).not.toBeInTheDocument(); + expect(screen.getAllByRole('checkbox')).toHaveLength(3); + }); }); diff --git a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx index 8dc3b6486..f41f2e5c6 100644 --- a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx +++ b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx @@ -20,6 +20,7 @@ export function EnvironmentRepositorySelector({ selectedRepositoryIds, onToggleRepository, onCreateRepository, + showSearch = true, inputPrefix = 'environment-repository', heightClassName = 'max-h-[11.5rem] md:h-[18.75rem]', }: { @@ -27,6 +28,7 @@ export function EnvironmentRepositorySelector({ selectedRepositoryIds: string[]; onToggleRepository: (repositoryId: string) => void; onCreateRepository?: () => void; + showSearch?: boolean; inputPrefix?: string; heightClassName?: string; }) { @@ -54,7 +56,7 @@ export function EnvironmentRepositorySelector({ return (
- {repositories.length > 0 ? ( + {showSearch && repositories.length > 0 ? (
From d00f7a5a978192de7eba955cdf4e685260a6909f Mon Sep 17 00:00:00 2001 From: "@tomny-dev" <20028678+tomny-dev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:37:25 +0000 Subject: [PATCH 3/3] refactor: simplify environment repository search --- .../src/app/(onboarding)/setup/StepRepoSelection.tsx | 1 - .../environments/EnvironmentRepositorySelector.tsx | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx index 08449b956..ce74e3b60 100644 --- a/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepRepoSelection.tsx @@ -359,7 +359,6 @@ export function StepRepoSelection({ selectedRepositoryIds={selectedRepositoryIds} onToggleRepository={toggleRepository} onCreateRepository={() => setCreateRepoDialogOpen(true)} - showSearch={false} inputPrefix="setup-repository" heightClassName="h-auto" /> diff --git a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx index f41f2e5c6..0b7d2926e 100644 --- a/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx +++ b/apps/web/src/components/settings/environments/EnvironmentRepositorySelector.tsx @@ -33,7 +33,7 @@ export function EnvironmentRepositorySelector({ heightClassName?: string; }) { const [search, setSearch] = useState(''); - const deferredSearch = useDeferredValue(search); + const normalizedSearch = useDeferredValue(search.trim().toLowerCase()); const sortedRepositories = useMemo( () => [...repositories].sort((left, right) => @@ -42,16 +42,14 @@ export function EnvironmentRepositorySelector({ [repositories], ); const filteredRepositories = useMemo(() => { - const query = deferredSearch.trim().toLowerCase(); - - if (!query) { + if (!normalizedSearch) { return sortedRepositories; } return sortedRepositories.filter((repository) => - repository.fullName.toLowerCase().includes(query), + repository.fullName.toLowerCase().includes(normalizedSearch), ); - }, [deferredSearch, sortedRepositories]); + }, [normalizedSearch, sortedRepositories]); return ( @@ -83,7 +81,7 @@ export function EnvironmentRepositorySelector({

) : null} - {filteredRepositories.length === 0 && deferredSearch.trim() ? ( + {filteredRepositories.length === 0 && normalizedSearch ? (

No repositories found.