feat(frontend): add header, filter bar, and sorting store for files page
- sorting.ts: per-section sort store (sort field + order) persisted to localStorage
- dsl.ts: build/parse DSL filter strings ({t=uuid,&,|,!,...})
- Header.svelte: sort dropdown, asc/desc toggle, filter toggle button
- FilterBar.svelte: tag token picker with operator buttons, search, apply/reset
- files/+page.svelte: wired header + filter bar, resets pagination on sort/filter change
- vite-mock-plugin.ts: added 5 mock tags for filter bar development
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api/client';
|
||||
import { ApiError } from '$lib/api/client';
|
||||
import FileCard from '$lib/components/file/FileCard.svelte';
|
||||
import FilterBar from '$lib/components/file/FilterBar.svelte';
|
||||
import Header from '$lib/components/layout/Header.svelte';
|
||||
import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte';
|
||||
import { fileSorting, type FileSortField } from '$lib/stores/sorting';
|
||||
import { parseDslFilter } from '$lib/utils/dsl';
|
||||
import type { File, FileCursorPage } from '$lib/api/types';
|
||||
|
||||
const LIMIT = 50;
|
||||
|
||||
const FILE_SORT_OPTIONS = [
|
||||
{ value: 'created', label: 'Created' },
|
||||
{ value: 'content_datetime', label: 'Date taken' },
|
||||
{ value: 'original_name', label: 'Name' },
|
||||
{ value: 'mime', label: 'Type' },
|
||||
];
|
||||
|
||||
let files = $state<File[]>([]);
|
||||
let nextCursor = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let hasMore = $state(true);
|
||||
let error = $state('');
|
||||
let filterOpen = $state(false);
|
||||
|
||||
// Derive current filter from URL ?filter= param
|
||||
let filterParam = $derived($page.url.searchParams.get('filter'));
|
||||
let activeTokens = $derived(parseDslFilter(filterParam));
|
||||
|
||||
// Track sort/order from store
|
||||
let sortState = $derived($fileSorting);
|
||||
|
||||
// Reset + reload whenever sort or filter changes
|
||||
let resetKey = $derived(`${sortState.sort}|${sortState.order}|${filterParam ?? ''}`);
|
||||
let prevKey = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (resetKey !== prevKey) {
|
||||
prevKey = resetKey;
|
||||
files = [];
|
||||
nextCursor = null;
|
||||
hasMore = true;
|
||||
error = '';
|
||||
}
|
||||
});
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
@@ -19,13 +54,18 @@
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: String(LIMIT) });
|
||||
const params = new URLSearchParams({
|
||||
limit: String(LIMIT),
|
||||
sort: sortState.sort,
|
||||
order: sortState.order,
|
||||
});
|
||||
if (nextCursor) params.set('cursor', nextCursor);
|
||||
if (filterParam) params.set('filter', filterParam);
|
||||
|
||||
const page = await api.get<FileCursorPage>(`/files?${params}`);
|
||||
files = [...files, ...(page.items ?? [])];
|
||||
nextCursor = page.next_cursor ?? null;
|
||||
hasMore = !!page.next_cursor;
|
||||
const res = await api.get<FileCursorPage>(`/files?${params}`);
|
||||
files = [...files, ...(res.items ?? [])];
|
||||
nextCursor = res.next_cursor ?? null;
|
||||
hasMore = !!res.next_cursor;
|
||||
} catch (err) {
|
||||
error = err instanceof ApiError ? err.message : 'Failed to load files';
|
||||
hasMore = false;
|
||||
@@ -33,6 +73,17 @@
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter(filter: string | null) {
|
||||
const url = new URL($page.url);
|
||||
if (filter) {
|
||||
url.searchParams.set('filter', filter);
|
||||
} else {
|
||||
url.searchParams.delete('filter');
|
||||
}
|
||||
goto(url.toString(), { replaceState: true });
|
||||
filterOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -40,6 +91,24 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<Header
|
||||
sortOptions={FILE_SORT_OPTIONS}
|
||||
sort={sortState.sort}
|
||||
order={sortState.order}
|
||||
filterActive={activeTokens.length > 0 || filterOpen}
|
||||
onSortChange={(s) => fileSorting.setSort(s as FileSortField)}
|
||||
onOrderToggle={() => fileSorting.toggleOrder()}
|
||||
onFilterToggle={() => (filterOpen = !filterOpen)}
|
||||
/>
|
||||
|
||||
{#if filterOpen}
|
||||
<FilterBar
|
||||
value={filterParam}
|
||||
onApply={applyFilter}
|
||||
onClose={() => (filterOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<main>
|
||||
{#if error}
|
||||
<p class="error" role="alert">{error}</p>
|
||||
|
||||
Reference in New Issue
Block a user