Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)
- [EE] Fixed Ask Sourcebot mermaid diagrams overflowing their container by contain-fitting them to both width and height, and made revealing a diagram from the answer jump it into view instantly to avoid over/undershooting. [#1373](https://github.com/sourcebot-dev/sourcebot/pull/1373)
- Passed Zoekt index parameters via argv to preserve revision names with punctuation. [#1376](https://github.com/sourcebot-dev/sourcebot/pull/1376)
- Kept selected language filters visible while narrowing the filter list. [#1377](https://github.com/sourcebot-dev/sourcebot/pull/1377)

## [5.0.4] - 2026-06-18

Expand Down
24 changes: 14 additions & 10 deletions packages/web/src/app/(app)/search/components/filterPanel/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ export const Entry = ({
countText = "999+";
}
return (
<div
<button
type="button"
aria-pressed={isSelected}
disabled={isDisabled && !isSelected}
className={clsx(
"flex flex-row items-center justify-between py-0.5 px-1 cursor-pointer rounded-md gap-2 select-none",
"flex w-full flex-row items-center justify-between py-0.5 px-1 cursor-pointer rounded-md gap-2 select-none text-left",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
{
"hover:bg-gray-200 dark:hover:bg-gray-700": !isSelected,
"bg-blue-200 dark:bg-blue-400": isSelected,
Expand All @@ -45,25 +49,25 @@ export const Entry = ({
)}
onClick={() => onClicked()}
>
<div className="flex flex-row items-center gap-1 overflow-hidden min-w-0">
<span className="flex flex-row items-center gap-1 overflow-hidden min-w-0">
{Icon ? Icon : (
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
)}
<div className="overflow-hidden flex-1 min-w-0">
<span className="overflow-hidden flex-1 min-w-0">
<Tooltip>
<TooltipTrigger asChild>
<p className="overflow-hidden text-ellipsis whitespace-nowrap truncate-start"><span>{displayName}</span></p>
<span className="block overflow-hidden text-ellipsis whitespace-nowrap truncate-start">{displayName}</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-sm">
<p className="font-mono text-sm break-all whitespace-pre-wrap">{displayName}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="px-2 py-0.5 bg-accent text-sm rounded-md flex-shrink-0">
</span>
</span>
<span className="px-2 py-0.5 bg-accent text-sm rounded-md flex-shrink-0">
{countText}
</div>
</div>
</span>
</button>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Filter } from './filter';
import type { Entry } from './entry';

afterEach(() => {
cleanup();
});

const entries: Entry[] = [
{
key: 'powershell',
displayName: 'PowerShell',
count: 4,
isSelected: true,
isHidden: false,
isDisabled: false,
},
{
key: 'c',
displayName: 'C',
count: 9,
isSelected: false,
isHidden: false,
isDisabled: false,
},
{
key: 'go',
displayName: 'Go',
count: 12,
isSelected: false,
isHidden: false,
isDisabled: false,
},
];

describe('Filter', () => {
test('renders entries as accessible toggle buttons', () => {
const onEntryClicked = vi.fn();

render(
<TooltipProvider>
<Filter
title="Languages"
searchPlaceholder="Search languages"
entries={entries}
onEntryClicked={onEntryClicked}
isStreaming={false}
/>
</TooltipProvider>,
);

const selectedEntry = screen.getByRole('button', { name: /PowerShell/ });
const unselectedEntry = screen.getByRole('button', { name: /C/ });

expect(selectedEntry.getAttribute('aria-pressed')).toBe('true');
expect(unselectedEntry.getAttribute('aria-pressed')).toBe('false');

fireEvent.click(selectedEntry);

expect(onEntryClicked).toHaveBeenCalledWith('powershell');
});

test('keeps selected entries visible and pinned while filtering', () => {
render(
<TooltipProvider>
<Filter
title="Languages"
searchPlaceholder="Search languages"
entries={entries}
onEntryClicked={() => undefined}
isStreaming={false}
/>
</TooltipProvider>,
);

fireEvent.change(screen.getByPlaceholderText('Search languages'), {
target: { value: 'C' },
});

const selectedEntry = screen.getByText('PowerShell');
const matchedEntry = screen.getByText('C');

expect(selectedEntry).toBeTruthy();
expect(matchedEntry).toBeTruthy();
expect(screen.queryByText('Go')).toBeNull();
expect(
selectedEntry.compareDocumentPosition(matchedEntry) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,19 @@ export const Filter = ({
threshold: 0.3,
});

const selectedEntries = entries.filter((entry) => entry.isSelected);
const result = fuse.search(searchFilter);
return result.map((result) => result.item);
const filteredEntries = new Map<string, Entry>();

for (const entry of selectedEntries) {
filteredEntries.set(entry.key, entry);
}

for (const { item } of result) {
filteredEntries.set(item.key, item);
}

return [...filteredEntries.values()];
}, [entries, searchFilter]);

return (
Expand All @@ -61,7 +72,7 @@ export const Filter = ({
<div
className="flex flex-col gap-0.5 text-sm overflow-scroll no-scrollbar"
>
{filteredEntries
{[...filteredEntries]
.sort((entryA, entryB) => compareEntries(entryB, entryA))
.map((entry) => (
<Entry
Expand Down