diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fd05b6a..31fc2eec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Silenced a false-positive `MaxListenersExceededWarning` logged on every request proxied through an external rewrite. [#1697](https://github.com/sourcebot-dev/sourcebot/pull/1697) +- Fixed selected language and repository filters disappearing when filtering the available options. [#1685](https://github.com/sourcebot-dev/sourcebot/pull/1685) ## [5.1.15] - 2026-09-29 diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx new file mode 100644 index 000000000..04d72367b --- /dev/null +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx @@ -0,0 +1,77 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { Filter } from './filter'; +import type { Entry } from './entry'; + +afterEach(cleanup); + +const createEntry = (displayName: string, isSelected = false, count = 1): Entry => ({ + key: displayName, + displayName, + count, + isSelected, + isHidden: false, + isDisabled: false, +}); + +const renderFilter = (entries: Entry[], onEntryClicked = vi.fn()) => render( + + + , +); + +describe('search filters', () => { + it('keeps selected languages above matches and allows clearing a hidden-by-search selection', () => { + const onEntryClicked = vi.fn(); + renderFilter([ + createEntry('C', false, 100), + createEntry('PowerShell', true), + createEntry('Python'), + ], onEntryClicked); + + fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'C' } }); + + const selected = screen.getByText('PowerShell'); + const match = screen.getByText('C'); + expect(selected.compareDocumentPosition(match) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.queryByText('Python')).toBeNull(); + + fireEvent.click(selected); + expect(onEntryClicked).toHaveBeenCalledExactlyOnceWith('PowerShell'); + }); + + it('shows a selected entry once when it also matches the filter', () => { + renderFilter([createEntry('Python', true), createEntry('PowerShell')]); + + fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'Python' } }); + + expect(screen.getAllByText('Python')).toHaveLength(1); + expect(screen.queryByText('PowerShell')).toBeNull(); + }); + + it('preserves selected entries when the filter has no matches', () => { + renderFilter([createEntry('PowerShell', true), createEntry('Python')]); + + fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'zzzz' } }); + + expect(screen.getByText('PowerShell')).toBeTruthy(); + expect(screen.queryByText('Python')).toBeNull(); + }); + + it('does not reorder the caller\'s entries when sorting the unfiltered list', () => { + const entries = [createEntry('Python'), createEntry('PowerShell', true)]; + Object.freeze(entries); + + renderFilter(entries); + + expect(entries.map(entry => entry.key)).toEqual(['Python', 'PowerShell']); + expect(screen.getByText('PowerShell')).toBeTruthy(); + }); +}); diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx index 0a76a50e6..b2c093410 100644 --- a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx @@ -31,13 +31,14 @@ export const Filter = ({ return entries; } - const fuse = new Fuse(entries, { + const selectedEntries = entries.filter(entry => entry.isSelected); + const fuse = new Fuse(entries.filter(entry => !entry.isSelected), { keys: ["displayName"], threshold: 0.3, }); const result = fuse.search(searchFilter); - return result.map((result) => result.item); + return [...selectedEntries, ...result.map((result) => result.item)]; }, [entries, searchFilter]); return ( @@ -61,7 +62,7 @@ export const Filter = ({
- {filteredEntries + {[...filteredEntries] .sort((entryA, entryB) => compareEntries(entryB, entryA)) .map((entry) => (