fix(SearchAssembly64): update preset handling and enhance dialog for preset values

This commit is contained in:
Jaime Idolpx 2026-06-14 05:12:33 -04:00
parent 13072c3735
commit 07cd6a81cf

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Search, Loader2, HardDrive, Download, ChevronRight, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react'; import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav'; import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
@ -61,14 +61,34 @@ interface CategoryMapping {
[k: string]: unknown; [k: string]: unknown;
} }
interface Preset { interface PresetValue {
aqlKey: string;
name?: string; name?: string;
title?: string; id?: number;
query?: string;
aql?: string;
[k: string]: unknown; [k: string]: unknown;
} }
interface PresetGroup {
type: string; // 'repo' | 'category' | 'subcat' | 'rating' | 'type' | 'date' | 'latest' | 'sort' | 'order'
description: string; // Human-readable group label shown on the chip
values: PresetValue[];
}
// Each preset group maps to a search-token prefix. 'sort' and 'order' are not
// filters (they're result-ordering directives) so they get an empty prefix
// marker and the renderer skips them.
const PRESET_PREFIX: Record<string, string | null> = {
repo: 'repo',
category: 'category',
subcat: 'subcat',
rating: 'rating',
type: 'type',
date: 'year',
latest: 'added',
sort: null,
order: null,
};
// ─── Props ──────────────────────────────────────────────────────────────────── // ─── Props ────────────────────────────────────────────────────────────────────
interface SearchAssembly64Props { interface SearchAssembly64Props {
@ -143,7 +163,8 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
const [categoryFilter, setCategoryFilter] = useState<number | null>(() => _store.categoryFilter); const [categoryFilter, setCategoryFilter] = useState<number | null>(() => _store.categoryFilter);
const [categories, setCategories] = useState<CategoryMapping[]>([]); const [categories, setCategories] = useState<CategoryMapping[]>([]);
const [presets, setPresets] = useState<Preset[]>([]); const [presets, setPresets] = useState<PresetGroup[]>([]);
const [activePreset, setActivePreset] = useState<PresetGroup | null>(null);
const [selectedItem, setSelectedItem] = useState<ContentItem | null>(null); const [selectedItem, setSelectedItem] = useState<ContentItem | null>(null);
const [entries, setEntries] = useState<ContentEntry[] | null>(null); const [entries, setEntries] = useState<ContentEntry[] | null>(null);
@ -204,10 +225,21 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
}; };
const handleSearch = () => doSearch(query, categoryFilter, 0); const handleSearch = () => doSearch(query, categoryFilter, 0);
const handleLoadMore = () => doSearch(query, categoryFilter, offset, true);
const applyPreset = (p: Preset) => { // Build an AQL token for a preset value and append/replace it in the query.
const q = (p.query ?? p.aql ?? '') as string; // Tokens that already contain a colon (e.g. 'subcat:c64comdemos') are
setQuery(q); // inserted verbatim; raw values get the group's prefix.
const applyPreset = (group: PresetGroup, value: PresetValue) => {
const prefix = PRESET_PREFIX[group.type];
setActivePreset(null);
if (prefix === null || prefix === undefined) return; // sort/order: not a filter
// Some aqlKeys are self-describing ("subcat:c64comdemos"); use them
// verbatim. Otherwise prepend the group's prefix.
const token = value.aqlKey.includes(':') ? value.aqlKey : `${prefix}:${value.aqlKey}`;
const trimmed = query.trim();
const next = trimmed ? `${trimmed} ${token}` : token;
setQuery(next);
doSearch(next);
doSearch(q, categoryFilter, 0); doSearch(q, categoryFilter, 0);
}; };
@ -218,8 +250,13 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
setEntries(null); setEntries(null);
setLoadingEntries(true); setLoadingEntries(true);
try { try {
const data = await leetJson<ContentEntry[]>(`/search/entries/${item.id}/${item.category}`); // The /search/entries endpoint now returns { contentEntry: [...] }
setEntries(data); // (matching the API's ContentEntryContainerV2 schema), not a bare array.
const data = await leetJson<ContentEntry[] | { contentEntry: ContentEntry[] }>(
`/search/entries/${item.id}/${item.category}`,
);
const list = Array.isArray(data) ? data : (data.contentEntry ?? []);
setEntries(list);
} catch (e: any) { } catch (e: any) {
toast.error(`Failed to load entries: ${e?.message ?? e}`); toast.error(`Failed to load entries: ${e?.message ?? e}`);
setEntries([]); setEntries([]);
@ -316,18 +353,18 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
{/* Presets */} {/* Presets */}
{presets.length > 0 && ( {presets.length > 0 && (
<div className="flex gap-1.5 mb-2 overflow-x-auto pb-0.5 scrollbar-none"> <div className="flex gap-1.5 mb-2 overflow-x-auto pb-0.5 scrollbar-none">
{presets.map((p, i) => { {presets
const label = (p.name ?? p.title ?? `Preset ${i + 1}`) as string; .filter(group => PRESET_PREFIX[group.type] !== null) // hide sort/order for now
return ( .map((group, i) => (
<button <button
key={i} key={`${group.type}-${i}`}
onClick={() => applyPreset(p)} onClick={() => setActivePreset(group)}
className="flex-shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 transition-colors" className="flex-shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 transition-colors inline-flex items-center gap-1"
> >
{label} {group.description}
<ChevronDown className="w-3 h-3 opacity-60" />
</button> </button>
); ))}
})}
</div> </div>
)} )}
@ -618,6 +655,33 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Preset values dialog */}
<Dialog open={activePreset !== null} onOpenChange={open => !open && setActivePreset(null)}>
<DialogContent className="max-w-sm flex flex-col max-h-[90dvh]">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{activePreset?.description ?? 'Preset'}</DialogTitle>
<DialogDescription>Tap a value to add it to your search.</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto flex-1 min-h-0 flex flex-col gap-1 py-1">
{activePreset?.values.map((v, i) => {
const label = v.name ?? v.aqlKey;
const prefix = PRESET_PREFIX[activePreset.type];
const token = v.aqlKey.includes(':') ? v.aqlKey : `${prefix}:${v.aqlKey}`;
return (
<button
key={`${v.aqlKey}-${i}`}
onClick={() => applyPreset(activePreset, v)}
className="w-full text-left px-4 py-2.5 rounded-lg border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 transition-colors flex items-center gap-2"
>
<span className="text-sm text-neutral-800 flex-1 truncate">{label}</span>
<span className="font-mono text-xs text-neutral-400 flex-shrink-0">{token}</span>
</button>
);
})}
</div>
</DialogContent>
</Dialog>
</> </>
); );
} }