Skip to content

Commit 040641c

Browse files
fix(web): keep selected search filters visible (#1685)
* fix(web): keep selected search filters visible * chore: add changelog entry for #1685 --------- Co-authored-by: Brendan Kellam <brendan@sourcebot.dev>
1 parent ea60fb1 commit 040641c

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Upgraded `@modelcontextprotocol/sdk` to `^1.32.1`. [#1719](https://github.com/sourcebot-dev/sourcebot/pull/1719)
2626
- Decoded percent-encoded characters in repo names derived from direct generic git URLs. [#1666](https://github.com/sourcebot-dev/sourcebot/pull/1666)
2727
- Fixed `.gitattributes` `linguist-language` overrides being ignored for files in subdirectories. [#1692](https://github.com/sourcebot-dev/sourcebot/pull/1692)
28+
- Fixed selected language and repository filters disappearing when filtering the available options. [#1685](https://github.com/sourcebot-dev/sourcebot/pull/1685)
2829

2930
## [5.1.15] - 2026-09-29
3031

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
import { TooltipProvider } from '@/components/ui/tooltip';
4+
import { Filter } from './filter';
5+
import type { Entry } from './entry';
6+
7+
afterEach(cleanup);
8+
9+
const createEntry = (displayName: string, isSelected = false, count = 1): Entry => ({
10+
key: displayName,
11+
displayName,
12+
count,
13+
isSelected,
14+
isHidden: false,
15+
isDisabled: false,
16+
});
17+
18+
const renderFilter = (entries: Entry[], onEntryClicked = vi.fn()) => render(
19+
<TooltipProvider>
20+
<Filter
21+
title="Languages"
22+
searchPlaceholder="Filter languages"
23+
entries={entries}
24+
onEntryClicked={onEntryClicked}
25+
isStreaming={false}
26+
/>
27+
</TooltipProvider>,
28+
);
29+
30+
describe('search filters', () => {
31+
it('keeps selected languages above matches and allows clearing a hidden-by-search selection', () => {
32+
const onEntryClicked = vi.fn();
33+
renderFilter([
34+
createEntry('C', false, 100),
35+
createEntry('PowerShell', true),
36+
createEntry('Python'),
37+
], onEntryClicked);
38+
39+
fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'C' } });
40+
41+
const selected = screen.getByText('PowerShell');
42+
const match = screen.getByText('C');
43+
expect(selected.compareDocumentPosition(match) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
44+
expect(screen.queryByText('Python')).toBeNull();
45+
46+
fireEvent.click(selected);
47+
expect(onEntryClicked).toHaveBeenCalledExactlyOnceWith('PowerShell');
48+
});
49+
50+
it('shows a selected entry once when it also matches the filter', () => {
51+
renderFilter([createEntry('Python', true), createEntry('PowerShell')]);
52+
53+
fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'Python' } });
54+
55+
expect(screen.getAllByText('Python')).toHaveLength(1);
56+
expect(screen.queryByText('PowerShell')).toBeNull();
57+
});
58+
59+
it('preserves selected entries when the filter has no matches', () => {
60+
renderFilter([createEntry('PowerShell', true), createEntry('Python')]);
61+
62+
fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'zzzz' } });
63+
64+
expect(screen.getByText('PowerShell')).toBeTruthy();
65+
expect(screen.queryByText('Python')).toBeNull();
66+
});
67+
68+
it('does not reorder the caller\'s entries when sorting the unfiltered list', () => {
69+
const entries = [createEntry('Python'), createEntry('PowerShell', true)];
70+
Object.freeze(entries);
71+
72+
renderFilter(entries);
73+
74+
expect(entries.map(entry => entry.key)).toEqual(['Python', 'PowerShell']);
75+
expect(screen.getByText('PowerShell')).toBeTruthy();
76+
});
77+
});

‎packages/web/src/app/(app)/search/components/filterPanel/filter.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@ export const Filter = ({
3131
return entries;
3232
}
3333

34-
const fuse = new Fuse(entries, {
34+
const selectedEntries = entries.filter(entry => entry.isSelected);
35+
const fuse = new Fuse(entries.filter(entry => !entry.isSelected), {
3536
keys: ["displayName"],
3637
threshold: 0.3,
3738
});
3839

3940
const result = fuse.search(searchFilter);
40-
return result.map((result) => result.item);
41+
return [...selectedEntries, ...result.map((result) => result.item)];
4142
}, [entries, searchFilter]);
4243

4344
return (
@@ -61,7 +62,7 @@ export const Filter = ({
6162
<div
6263
className="flex flex-col gap-0.5 text-sm overflow-scroll no-scrollbar"
6364
>
64-
{filteredEntries
65+
{[...filteredEntries]
6566
.sort((entryA, entryB) => compareEntries(entryB, entryA))
6667
.map((entry) => (
6768
<Entry

0 commit comments

Comments
 (0)