diff --git a/src/components/admin/studio/batch-progress.tsx b/src/components/admin/studio/batch-progress.tsx index 9c6867f2..09d514b0 100644 --- a/src/components/admin/studio/batch-progress.tsx +++ b/src/components/admin/studio/batch-progress.tsx @@ -1,7 +1,8 @@ "use client"; -import { Check, Loader2, X } from "lucide-react"; +import { Ban, Check, Loader2, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { BatchItemStatus } from "./studio-types"; @@ -10,18 +11,30 @@ export function BatchProgress({ done, succeeded, failed, + cancelling, + onCancel, + onDismiss, }: { progress: Map; done: boolean; succeeded: number; failed: number; + cancelling?: boolean; + onCancel?: () => void; + onDismiss?: () => void; }) { const entries = [...progress.entries()]; + const total = entries.length; + const completed = entries.filter( + ([, s]) => s.status === "done" || s.status === "failed", + ).length; + const percent = total > 0 ? Math.round((completed / total) * 100) : 0; + return (
-
+
- Importing… + {done ? "Import finished" : "Importing…"} {done ? ( - ✓ {succeeded} ok{failed > 0 ? ` · ${failed} failed` : ""} + {completed}/{total} · {succeeded} ok + {failed > 0 ? ` · ${failed} failed` : ""} ) : ( - + + + {completed}/{total} + + + )}
+ + {!done && ( +
+
+
+ )} +
{entries.map(([classname, s]) => (
@@ -66,6 +95,29 @@ export function BatchProgress({
))}
+ +
+ {done ? ( + + ) : ( + + )} +
); } diff --git a/src/components/admin/studio/catalog-rail.tsx b/src/components/admin/studio/catalog-rail.tsx index 67e364a8..8edf9d45 100644 --- a/src/components/admin/studio/catalog-rail.tsx +++ b/src/components/admin/studio/catalog-rail.tsx @@ -67,7 +67,7 @@ export function CatalogRail({ onSelect(selectedId === node.id ? null : node.id); }} className={cn( - "group flex w-full items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors", + "group flex w-full cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]", selectedId === node.id ? "bg-[var(--admin-accent)]/15 text-[var(--admin-text)]" : "text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10 hover:text-[var(--admin-text)]", diff --git a/src/components/admin/studio/studio-client.tsx b/src/components/admin/studio/studio-client.tsx index 1685993e..16114b1f 100644 --- a/src/components/admin/studio/studio-client.tsx +++ b/src/components/admin/studio/studio-client.tsx @@ -105,6 +105,7 @@ export function StudioClient({ const [batchDone, setBatchDone] = useState(false); const [batchSucceeded, setBatchSucceeded] = useState(0); const [batchFailed, setBatchFailed] = useState(0); + const [batchCancelling, setBatchCancelling] = useState(false); const batchAbortRef = useRef(null); // Nitro editor @@ -152,13 +153,13 @@ export function StudioClient({ }, []); const fetchItems = useCallback( - async (search = activeSearch, page = 1) => { + async (search: string, page: number, source?: string) => { setLoading(true); setError(""); try { const qs = new URLSearchParams({ page: String(page) }); if (search) qs.set("search", search); - if (activeSource) qs.set("source", activeSource); + if (source) qs.set("source", source); const res = await adminFetch( `/api/admin/import/furni?${qs.toString()}`, ); @@ -180,7 +181,7 @@ export function StudioClient({ setLoading(false); } }, - [activeSearch, activeSource], + [], ); const changeSource = (sourceId: string) => { @@ -192,7 +193,6 @@ export function StudioClient({ setItems([]); setActiveSearch(""); setSearchTerm(""); - fetchItems("", 1); }; const fetchSources = useCallback(async () => { @@ -208,10 +208,10 @@ export function StudioClient({ }, []); useEffect(() => { - fetchItems(); + fetchItems("", 1, activeSource); fetchStats(); fetchSources(); - }, [fetchItems, fetchStats, fetchSources]); + }, [fetchItems, fetchStats, fetchSources, activeSource]); // Keep detail in sync with the refreshed list. useEffect(() => { @@ -220,9 +220,15 @@ export function StudioClient({ if (updated) setDetail(updated); }, [items, detail]); + const clearSearch = () => { + setSearchTerm(""); + setActiveSearch(""); + fetchItems("", 1, activeSource); + }; + const submitSearch = () => { setActiveSearch(searchTerm); - fetchItems(searchTerm, 1); + fetchItems(searchTerm, 1, activeSource); }; function toggleSelect(classname: string) { @@ -305,6 +311,7 @@ export function StudioClient({ setBatchDone(false); setBatchSucceeded(0); setBatchFailed(0); + setBatchCancelling(false); const initial = new Map(); for (const i of toImport) { @@ -394,12 +401,20 @@ export function StudioClient({ } } finally { batchAbortRef.current = null; + setBatchCancelling(false); setSelected(new Set()); fetchStats(); if (!batchDone) setBatchProgress(null); } } + const cancelBatch = () => { + if (batchAbortRef.current) { + setBatchCancelling(true); + batchAbortRef.current.abort(); + } + }; + async function deleteItem() { if (!deleting) return; try { @@ -444,7 +459,7 @@ export function StudioClient({ return (
{/* ── Header ─────────────────────────────────────────── */} @@ -474,8 +489,18 @@ export function StudioClient({ onChange={(e) => setSearchTerm(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitSearch()} placeholder="Search furni, classnames, keywords…" - className="pl-8" + className="pl-8 pr-8" /> + {searchTerm ? ( + + ) : null}
+ {selected.size > 0 && ( + + {selected.size} selected + + )} + + {selected.size > 0 && ( + + )} +
{selectedNonImported > 0 && ( @@ -574,7 +616,7 @@ export function StudioClient({
{/* ── Body: rail + grid + drawer ────────────────────── */} -
+
{ const isSel = selected.has(item.classname); return ( - -
-
-
-

- {item.name} -

-

- {item.classname} -

-
-
-
- - {TYPE_LABELS[item.type] ?? item.type} - - {item.alreadyImported ? ( - - {item.nitroExists ? "Imported" : "No nitro"} - - ) : ( - - New - +
+
- + > +
+ {item.name} handleImgError(e, item)} + /> +
+
+
+

+ {item.name} +

+

+ {item.classname} +

+
+
+
+ + {TYPE_LABELS[item.type] ?? item.type} + + {item.alreadyImported ? ( + + {item.nitroExists ? "Imported" : "No nitro"} + + ) : ( + + New + + )} +
+ + +
); })}
@@ -690,7 +739,11 @@ export function StudioClient({