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 { 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user