feat(frontend): merge top-bar search with the filter bar, expose kind/created filters
Unify the upstream top-bar search box and the local SearchFilterBar into one filtering capability sharing a single state store, with the URL as its projection: - lib/stores/filter.svelte.ts: module-level $state store, the single source of truth; every surface mutates the same proxy in place - lib/composables/useFilterUrlSync.svelte.ts: one $effect serves both directions (URL -> store adoption, store -> URL replaceState write) with echo suppression so the loop settles in one round-trip - FilterFields.svelte: the five selects (type/size/modified/created/ kind) + recursive switch; word lists and i18n labels live in one place; SearchFilterBar reuses it instead of its own copies - TopBarFilterPanel.svelte: dropdown panel anchored inside the top-bar search input (in-field toggle button with active-count badge); Escape/outside-click close, no overlay stacking with suggestions - FilterChips.svelte: dismissible chips on /search, which is now URL-driven and loses its three inline <select>s - New dimensions the backend already supported: kind (file/folder, resource_types) and created (created_after); URL keys q/type/size/ date/created/kind/recursive, non-defaults only, unknown values degrade to defaults - /files keeps in-place filtering (no navigation); folderTarget carries filter params through folder entry; ?file= and in/scope survive filter writes - topbar.css: input right padding reserves the three in-field controls Backend unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -88,3 +88,49 @@ it('submits a search and routes to /search', async () => {
|
||||
await fireEvent.click(screen.getByTestId('appshell-search-submit-btn'));
|
||||
await waitFor(() => expect(goto).toHaveBeenCalledWith('/search?q=report'));
|
||||
});
|
||||
|
||||
it('opens the filter panel, and its presets ride along on submit', async () => {
|
||||
render(AppShell, { props: { children } });
|
||||
// Panel opens from the toggle inside the search input…
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
expect(screen.getByTestId('appshell-filter-panel')).toBeTruthy();
|
||||
// …and picking a preset lights the toggle's badge (shared store).
|
||||
await fireEvent.change(screen.getByTestId('filter-type-select'), {
|
||||
target: { value: 'image' }
|
||||
});
|
||||
expect(screen.getByTestId('appshell-filter-toggle-btn').textContent).toContain('1');
|
||||
// Submitting carries the preset into the search URL.
|
||||
const input = screen.getByTestId('appshell-search-input');
|
||||
await fireEvent.input(input, { target: { value: 'report' } });
|
||||
await fireEvent.click(screen.getByTestId('appshell-search-submit-btn'));
|
||||
await waitFor(() => expect(goto).toHaveBeenCalledWith('/search?q=report&type=image'));
|
||||
// Submit closes the panel; reopen it and "Clear filters" resets the
|
||||
// presets — the badge drops and the next submit is param-free.
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-clear-btn'));
|
||||
expect(screen.getByTestId('appshell-filter-toggle-btn').textContent!.includes('1')).toBe(false);
|
||||
await fireEvent.input(input, { target: { value: 'again' } });
|
||||
await fireEvent.click(screen.getByTestId('appshell-search-submit-btn'));
|
||||
await waitFor(() => expect(goto).toHaveBeenCalledWith('/search?q=again'));
|
||||
// Leave the shared store clean for the other tests in this file.
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-clear-btn'));
|
||||
});
|
||||
|
||||
it('closes the filter panel on Escape and on outside click', async () => {
|
||||
render(AppShell, { props: { children } });
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
expect(screen.getByTestId('appshell-filter-panel')).toBeTruthy();
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-done-btn'));
|
||||
expect(screen.queryByTestId('appshell-filter-panel')).toBeNull();
|
||||
// Outside click (window-level handler) reopens-then-closes too.
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
expect(screen.getByTestId('appshell-filter-panel')).toBeTruthy();
|
||||
await fireEvent.click(screen.getByTestId('shell-child'));
|
||||
expect(screen.queryByTestId('appshell-filter-panel')).toBeNull();
|
||||
// Escape closes it as well.
|
||||
await fireEvent.click(screen.getByTestId('appshell-filter-toggle-btn'));
|
||||
expect(screen.getByTestId('appshell-filter-panel')).toBeTruthy();
|
||||
await fireEvent.keyDown(window, { key: 'Escape' });
|
||||
expect(screen.queryByTestId('appshell-filter-panel')).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user