fix(SearchAssembly64): update preset handling and enhance dialog for preset values
This commit is contained in:
parent
13072c3735
commit
07cd6a81cf
|
|
@ -1,5 +1,5 @@
|
|||
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 { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
|
|
@ -61,14 +61,34 @@ interface CategoryMapping {
|
|||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
interface Preset {
|
||||
interface PresetValue {
|
||||
aqlKey: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
query?: string;
|
||||
aql?: string;
|
||||
id?: number;
|
||||
[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 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SearchAssembly64Props {
|
||||
|
|
@ -143,7 +163,8 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
const [categoryFilter, setCategoryFilter] = useState<number | null>(() => _store.categoryFilter);
|
||||
|
||||
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 [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 handleLoadMore = () => doSearch(query, categoryFilter, offset, true);
|
||||
const applyPreset = (p: Preset) => {
|
||||
const q = (p.query ?? p.aql ?? '') as string;
|
||||
setQuery(q);
|
||||
|
||||
// Build an AQL token for a preset value and append/replace it in the query.
|
||||
// Tokens that already contain a colon (e.g. 'subcat:c64comdemos') are
|
||||
// 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);
|
||||
};
|
||||
|
||||
|
|
@ -218,8 +250,13 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
setEntries(null);
|
||||
setLoadingEntries(true);
|
||||
try {
|
||||
const data = await leetJson<ContentEntry[]>(`/search/entries/${item.id}/${item.category}`);
|
||||
setEntries(data);
|
||||
// The /search/entries endpoint now returns { contentEntry: [...] }
|
||||
// (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) {
|
||||
toast.error(`Failed to load entries: ${e?.message ?? e}`);
|
||||
setEntries([]);
|
||||
|
|
@ -316,18 +353,18 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
{/* Presets */}
|
||||
{presets.length > 0 && (
|
||||
<div className="flex gap-1.5 mb-2 overflow-x-auto pb-0.5 scrollbar-none">
|
||||
{presets.map((p, i) => {
|
||||
const label = (p.name ?? p.title ?? `Preset ${i + 1}`) as string;
|
||||
return (
|
||||
{presets
|
||||
.filter(group => PRESET_PREFIX[group.type] !== null) // hide sort/order for now
|
||||
.map((group, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => applyPreset(p)}
|
||||
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"
|
||||
key={`${group.type}-${i}`}
|
||||
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 inline-flex items-center gap-1"
|
||||
>
|
||||
{label}
|
||||
{group.description}
|
||||
<ChevronDown className="w-3 h-3 opacity-60" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -618,6 +655,33 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
</div>
|
||||
</DialogContent>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user