feat(SearchOverlay): add scroll position restoration and mounted paths tracking

This commit is contained in:
Jaime Idolpx 2026-06-14 01:31:47 -04:00
parent 6d12cebc05
commit 9a0268a6b4

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { X, Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, ArrowUp, ArrowDown, ArrowUpDown, HardDrive, FolderOpen } from 'lucide-react'; import { X, Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, ArrowUp, ArrowDown, ArrowUpDown, HardDrive, FolderOpen } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
@ -138,6 +138,7 @@ const _store = {
filterLanguage: null as string | null, filterLanguage: null as string | null,
sortField: 'name' as SortField, sortField: 'name' as SortField,
sortDir: 'asc' as SortDir, sortDir: 'asc' as SortDir,
scrollTop: 0,
}; };
function FilterChips({ function FilterChips({
@ -166,6 +167,7 @@ function FilterChips({
} }
export default function SearchOverlay({ config, setConfig, onClose, onOpenFolder }: SearchOverlayProps) { export default function SearchOverlay({ config, setConfig, onClose, onOpenFolder }: SearchOverlayProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState(() => _store.query); const [query, setQuery] = useState(() => _store.query);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [isScanning, setIsScanning] = useState(false); const [isScanning, setIsScanning] = useState(false);
@ -202,6 +204,26 @@ export default function SearchOverlay({ config, setConfig, onClose, onOpenFolder
_store.sortDir = sortDir; _store.sortDir = sortDir;
}, [query, results, hasSearched, showFilter, filterSystem, filterVideo, filterLanguage, sortField, sortDir]); }, [query, results, hasSearched, showFilter, filterSystem, filterVideo, filterLanguage, sortField, sortDir]);
// Restore scroll position after results render.
useEffect(() => {
if (_store.scrollTop > 0 && scrollRef.current) {
scrollRef.current.scrollTop = _store.scrollTop;
}
}, []);
// Build a set of fully-resolved paths currently mounted on any IEC device.
const mountedPaths = useMemo(() => {
const paths = new Set<string>();
for (const d of Object.values(config?.devices?.iec ?? {})) {
const dev = d as any;
if (!dev?.url) continue;
const base = (dev.base_url ?? '').replace(/\/$/, '');
const url = dev.url.startsWith('/') ? dev.url : '/' + dev.url;
paths.add(base ? base + url : dev.url);
}
return paths;
}, [config]);
const handleSearch = async () => { const handleSearch = async () => {
if (!query.trim()) { toast.error('Please enter a search term'); return; } if (!query.trim()) { toast.error('Please enter a search term'); return; }
setIsSearching(true); setIsSearching(true);
@ -426,17 +448,14 @@ export default function SearchOverlay({ config, setConfig, onClose, onOpenFolder
</div> </div>
</div> </div>
{/* DB status chip */}
{dbPhase === 'ready' && !busy && (
<p className="text-xs text-green-600 mt-2 flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
Database ready
</p>
)}
</div> </div>
{/* Body */} {/* Body */}
<div className="flex-1 overflow-y-auto"> <div
ref={scrollRef}
className="flex-1 overflow-y-auto"
onScroll={e => { _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }}
>
{/* Scanning progress */} {/* Scanning progress */}
{isScanning && scanPhase && ( {isScanning && scanPhase && (
<div className="flex flex-col items-center justify-center py-16 gap-3"> <div className="flex flex-col items-center justify-center py-16 gap-3">
@ -477,6 +496,7 @@ export default function SearchOverlay({ config, setConfig, onClose, onOpenFolder
entry={resultToEntry(result)} entry={resultToEntry(result)}
onPrimaryClick={() => setMountEntry(result)} onPrimaryClick={() => setMountEntry(result)}
onActionsClick={e => { e.stopPropagation(); setActionEntry(result); }} onActionsClick={e => { e.stopPropagation(); setActionEntry(result); }}
selected={mountedPaths.has(result.path)}
nameSlot={ nameSlot={
<> <>
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">