refactor(search): adapt UI to use ResourceList

This commit is contained in:
Edouard Vanbelle
2026-07-26 16:38:49 +02:00
parent c22741bc7f
commit c946cf4333
11 changed files with 739 additions and 309 deletions
+30 -10
View File
@@ -4,7 +4,7 @@
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { logout } from '$lib/api/endpoints/auth';
import { searchFiles } from '$lib/api/endpoints/search';
import { searchResources } from '$lib/api/endpoints/search';
import { fileInlineUrl, deleteFile } from '$lib/api/endpoints/files';
import { deleteFolder } from '$lib/api/endpoints/folders';
import { addFavorite } from '$lib/api/endpoints/favorites';
@@ -17,6 +17,7 @@
import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
import { apiFetch } from '$lib/api/client';
import { dialogs } from '$lib/stores/dialogs.svelte';
import { files as filesStore } from '$lib/stores/files.svelte';
import { preferences } from '$lib/stores/preferences.svelte';
import { session } from '$lib/stores/session.svelte';
import { theme, type Theme } from '$lib/stores/theme.svelte';
@@ -255,11 +256,22 @@
function goToResults() {
const q = searchQuery.trim();
if (q) {
suggestOpen = false;
searchActive = false;
goto(resolve(`/search?q=${encodeURIComponent(q)}`));
if (!q) return;
suggestOpen = false;
searchActive = false;
// Carry the currently-open folder into the search URL as `?in=<uuid>`
// so a hard refresh, a shared link, or a bookmark all restore the
// "This folder" scope. Trash section is always global — skip. See
// `/search/+page.svelte` for the receiver side.
//
// Built by hand instead of via `URLSearchParams` because the Svelte
// lint (svelte/prefer-svelte-reactivity) flags the mutable stdlib
// variant; the two params here don't need reactivity anyway.
const parts = [`q=${encodeURIComponent(q)}`];
if (filesStore.currentFolder && filesStore.section !== 'trash') {
parts.push(`in=${encodeURIComponent(filesStore.currentFolder)}`);
}
goto(resolve(`/search?${parts.join('&')}`));
}
function onSearch(e: SubmitEvent) {
@@ -285,12 +297,20 @@
suggestInflight = ctl;
suggestBusy = true;
try {
const r = await searchFiles(q, { recursive: true, limit: 6, signal: ctl.signal });
const r = await searchResources(q, { recursive: true, limit: 9, signal: ctl.signal });
if (seq !== suggestSeq) return; // superseded while awaiting
suggestions = [
...r.folders.slice(0, 3).map((item) => ({ kind: 'folder' as const, item })),
...r.files.slice(0, 6).map((item) => ({ kind: 'file' as const, item }))
];
// The wire is ordered — folders first, then files — but slice
// per kind explicitly so the header preview stays a folder-heavy
// list even when files dominate the result set.
const folders = r.items
.filter((it) => it.resource_type === 'folder')
.slice(0, 3)
.map((it) => ({ kind: 'folder' as const, item: it.resource as FolderItem }));
const files = r.items
.filter((it) => it.resource_type === 'file')
.slice(0, 6)
.map((it) => ({ kind: 'file' as const, item: it.resource as FileItem }));
suggestions = [...folders, ...files];
suggestOpen = suggestions.length > 0;
} catch {
if (seq !== suggestSeq || ctl.signal.aborted) return;
+3 -1
View File
@@ -9,7 +9,9 @@ const { goto, pageState } = vi.hoisted(() => ({
vi.mock('$app/navigation', () => ({ goto }));
vi.mock('$app/state', () => ({ page: pageState }));
vi.mock('$lib/api/endpoints/auth', () => ({ logout: vi.fn() }));
vi.mock('$lib/api/endpoints/search', () => ({ searchFiles: vi.fn(async () => ({ items: [] })) }));
vi.mock('$lib/api/endpoints/search', () => ({
searchResources: vi.fn(async () => ({ items: [], query_time_ms: 0 }))
}));
vi.mock('$lib/api/endpoints/files', () => ({ fileInlineUrl: () => '/in' }));
import { logout } from '$lib/api/endpoints/auth';
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { logout } from '$lib/api/endpoints/auth';
import { searchFiles } from '$lib/api/endpoints/search';
import { searchResources } from '$lib/api/endpoints/search';
import { fileInlineUrl } from '$lib/api/endpoints/files';
import Icon from '$lib/icons/Icon.svelte';
import { t } from '$lib/i18n/index.svelte';
@@ -193,24 +193,33 @@
}
searchTimer = setTimeout(async () => {
try {
const r = await searchFiles(q, { recursive: true, limit: 5 });
const folders: Command[] = r.folders.slice(0, 3).map((f) => ({
id: `fld-${f.id}`,
label: f.name,
icon: 'folder',
hint: t('files.folder', 'Folder'),
run: nav(`/files/${f.id}`)
}));
const files: Command[] = r.files.slice(0, 5).map((f) => ({
id: `fil-${f.id}`,
label: f.name,
icon: 'file',
hint: t('files.file', 'File'),
run: () => {
close();
window.open(fileInlineUrl(f.id), '_blank', 'noopener');
}
}));
const r = await searchResources(q, { recursive: true, limit: 8 });
// Wire items are ordered folders-first-then-files, but demux
// explicitly so the palette keeps the two-section layout even
// when file hits dominate the result set.
const folders: Command[] = r.items
.filter((it) => it.resource_type === 'folder')
.slice(0, 3)
.map((it) => ({
id: `fld-${it.resource.id}`,
label: it.resource.name,
icon: 'folder',
hint: t('files.folder', 'Folder'),
run: nav(`/files/${it.resource.id}`)
}));
const files: Command[] = r.items
.filter((it) => it.resource_type === 'file')
.slice(0, 5)
.map((it) => ({
id: `fil-${it.resource.id}`,
label: it.resource.name,
icon: 'file',
hint: t('files.file', 'File'),
run: () => {
close();
window.open(fileInlineUrl(it.resource.id), '_blank', 'noopener');
}
}));
fileMatches = [...folders, ...files];
} catch {
fileMatches = [];
@@ -4,11 +4,13 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
const { goto } = vi.hoisted(() => ({ goto: vi.fn() }));
vi.mock('$app/navigation', () => ({ goto }));
vi.mock('$lib/api/endpoints/auth', () => ({ logout: vi.fn() }));
vi.mock('$lib/api/endpoints/search', () => ({ searchFiles: vi.fn(async () => ({ items: [] })) }));
vi.mock('$lib/api/endpoints/search', () => ({
searchResources: vi.fn(async () => ({ items: [], query_time_ms: 0 }))
}));
vi.mock('$lib/api/endpoints/files', () => ({ fileInlineUrl: () => '/in' }));
vi.mock('$lib/stores/dialogs.svelte', () => ({ confirmDialog: vi.fn() }));
import { searchFiles } from '$lib/api/endpoints/search';
import { searchResources } from '$lib/api/endpoints/search';
import { session } from '$lib/stores/session.svelte';
import CommandPalette from './CommandPalette.svelte';
@@ -42,6 +44,6 @@ it('searches files as the query is typed', async () => {
await openPalette();
const input = await screen.findByTestId('command-palette-input');
await fireEvent.input(input, { target: { value: 'report' } });
await waitFor(() => expect(searchFiles).toHaveBeenCalled());
expect(m(searchFiles).mock.calls[0][0]).toBe('report');
await waitFor(() => expect(searchResources).toHaveBeenCalled());
expect(m(searchResources).mock.calls[0][0]).toBe('report');
});