refactor(search): normalize buttons to fit match other sections

This commit is contained in:
Edouard Vanbelle
2026-07-26 16:54:15 +02:00
parent 5a034d390e
commit e5fa234f3a
3 changed files with 88 additions and 23 deletions
+3
View File
@@ -25,6 +25,8 @@ export interface SearchOptions {
cursor?: string;
/** Sort dimension; maps to backend `order_by`. */
sortBy?: SortBy;
/** Reverse sort direction; maps to backend `reverse`. */
reverse?: boolean;
/** Restrict to files, folders, or both (default). */
resourceTypes?: ItemType[];
/** Abort the request when a newer search supersedes it. */
@@ -58,6 +60,7 @@ export function searchResources(
if (opts.limit != null) params.append('limit', String(opts.limit));
if (opts.cursor) params.append('cursor', opts.cursor);
if (opts.sortBy) params.append('order_by', opts.sortBy);
if (opts.reverse) params.append('reverse', 'true');
return apiJson<SearchResourcesResponse>(`/api/search?${params.toString()}`, {
credentials: 'same-origin',
signal: opts.signal
+9
View File
@@ -370,6 +370,15 @@ export const OxiIcons: Record<string, IconEntry> = {
512,
"M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
],
// Font Awesome Free 6.7.2 `ranking-star` (podium with a star crown).
// Hand-added — /search's group-by dropdown uses this glyph for the
// "Relevance" option (Ed's 2026-07-26 UX call). Registry header says
// "regenerate from the source" but no generator lives in-repo, so
// hand-inserting matches the shape of every other entry.
"ranking-star": [
640,
"M353.8 54.1L330.2 6.3c-3.9-8.3-16.1-8.3-20 0L286.6 54.1l-52.9 7.7c-9.2 1.3-12.9 12.7-6.2 19.3l38.3 37.3-9.1 52.7c-1.6 9.2 8.1 16.2 16.3 11.9l47.3-24.9 47.3 24.9c8.2 4.3 17.9-2.7 16.3-11.9l-9-52.7 38.3-37.3c6.7-6.5 3-17.9-6.2-19.3L353.8 54.1zM256 256c-17.7 0-32 14.3-32 32l0 192c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-192c0-17.7-14.3-32-32-32l-128 0zM32 320c-17.7 0-32 14.3-32 32L0 480c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32L32 320zm416 96l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32l-128 0c-17.7 0-32 14.3-32 32z"
],
"repeat": [
512,
"M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z"
+76 -23
View File
@@ -1,6 +1,10 @@
<script lang="ts">
import EmptyState from '$lib/components/EmptyState.svelte';
import ResourceList, { isFile, type ContextAction } from '$lib/components/ResourceList.svelte';
import ResourceList, {
isFile,
type ContextAction,
type GroupByDef
} from '$lib/components/ResourceList.svelte';
import { errorMessage, errorToast } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
@@ -8,7 +12,12 @@
import { searchResources } from '$lib/api/endpoints/search';
import { fileDownloadUrl, renameFile, deleteFile } from '$lib/api/endpoints/files';
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
import { addFavorite, removeFavorite } from '$lib/api/endpoints/favorites';
import {
addFavorite,
removeFavorite,
dateBucket,
sizeBucket
} from '$lib/api/endpoints/favorites';
import type { FileItem, FolderItem, SearchResourceItem, SortBy } from '$lib/api/types';
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
import { folderAccessCached, probeFolderAccess } from '$lib/utils/folderAccess';
@@ -65,7 +74,13 @@
let total = $state<number | undefined>(undefined);
let loading = $state(false);
let error = $state<string | null>(null);
let sortBy = $state<SortBy>('relevance');
// Sort dimension + direction are surfaced through ResourceList's
// built-in group-by selector (DisplayModeControls) rather than a
// standalone sort `<select>`, so /search matches /favorites /
// /recent / /trash. `groupBy` = active group-by key from the
// `groupBys` list below; `reversed` = the asc/desc toggle.
let groupBy = $state('');
let reversed = $state(false);
// Derived from the URL — URL is the single source of truth so refresh,
// bookmarks and shared links all restore the same scope. Rules:
// `?scope=all` → 'all' (explicit "Everywhere" toggle;
@@ -193,16 +208,42 @@
dateFilter = 'all';
}
const SORTS: { v: SortBy; l: string }[] = [
{ v: 'relevance', l: t('search.sort.relevance', 'Relevance') },
{ v: 'name', l: t('search.sort.name_asc', 'Name A-Z') },
{ v: 'name_desc', l: t('search.sort.name_desc', 'Name Z-A') },
{ v: 'date_desc', l: t('search.sort.newest', 'Newest') },
{ v: 'date', l: t('search.sort.oldest', 'Oldest') },
{ v: 'size_desc', l: t('search.sort.largest', 'Largest') },
{ v: 'size', l: t('search.sort.smallest', 'Smallest') }
// ── Group / sort dimensions (shown in the DisplayModeControls dropdown) ──
// Ed's 2026-07-26 spec: 4 options total —
// • Relevance (default, flat) — search's native ranking
// • Name (flat) — A-Z with the asc/desc toggle for Z-A
// • Size (grouped) — bucketed via the shared `sizeBucket`
// • Modified (grouped) — bucketed via the shared `dateBucket`
// Omitting `bucketOf` = flat list (see the ResourceList interface).
// `orderBy` values map 1:1 to the backend `SearchResourcesQuery.order_by`.
// The asc/desc button binds to `reversed` and passes through to the
// backend `reverse` flag.
const groupBys: GroupByDef[] = [
{
key: '',
label: t('search.sort.relevance', 'Relevance'),
orderBy: 'relevance',
icon: 'ranking-star'
},
{ key: 'name', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' },
{
key: 'size',
label: t('groupby.size', 'Size'),
orderBy: 'size',
bucketOf: (item) => sizeBucket(isFile(item) ? item.size : null)
},
{
key: 'modifiedAt',
label: t('groupby.modifiedAt', 'Modified date'),
orderBy: 'date',
bucketOf: (item) => dateBucket(item.modified_at)
}
];
function orderByForGroup(): string {
return groupBys.find((g) => g.key === groupBy)?.orderBy ?? 'relevance';
}
// Stale-response guard: rapid-fire query/filter/sort changes each start a
// full recursive backend search; without the token a SLOW earlier response
// could resolve after (and clobber) a newer one, and the superseded server
@@ -225,7 +266,8 @@
: undefined;
return {
recursive: true,
sortBy,
sortBy: orderByForGroup() as SortBy,
reverse: reversed,
folderId,
fileTypes: typeFilter === 'all' ? undefined : TYPE_EXT[typeFilter],
...sizeBounds(sizeFilter),
@@ -482,8 +524,9 @@
];
$effect(() => {
// re-run when query, sort, scope, or any filter changes
void sortBy;
// re-run when query, sort/direction, scope, or any filter changes
void groupBy;
void reversed;
void scope;
void typeFilter;
void sizeFilter;
@@ -562,6 +605,17 @@
onfavorite={toggleFavorite}
onshared={openShareDialog}
{contextActions}
{groupBys}
bind:groupBy
bind:reversed
onreload={() => {
// Group-by or asc/desc changed — reset pagination and let the
// `$effect` above pick up the new state on its next tick (it's
// already reactive on `groupBy` + `reversed`). Explicit
// `cursor = undefined` here just guarantees the in-flight
// `next_cursor` from the OLD sort can't feed a stale page 2.
cursor = undefined;
}}
menuPrepare={async (item) => {
// Lazy folder-access probe — fires only when the user opens
// the context menu on a row, not proactively for every row on
@@ -634,15 +688,14 @@
{#each DATES as o (o.v)}<option value={o.v} data-testid={`search-date-${o.v}`}>{o.l}</option
>{/each}
</select>
<select
class="sort-select"
bind:value={sortBy}
aria-label={t('search.sort_by', 'Sort by')}
data-testid="search-sort-select"
>
{#each SORTS as s (s.v)}<option value={s.v} data-testid={`search-sort-${s.v}`}>{s.l}</option
>{/each}
</select>
<!--
NOTE: sort dimension + asc/desc live in ResourceList's
built-in DisplayModeControls now (fed by `groupBys` +
`bind:groupBy` + `bind:reversed` below), matching
/favorites / /recent / /trash. The old
`<select bind:value={sortBy}>` was removed with the
`SORTS` array.
-->
{#if hasFilters}
<button class="clear-filters" data-testid="search-clear-filters-btn" onclick={clearFilters}>
<Icon name="times" />