refactor(search): normalize buttons to fit match other sections
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user