feat: Studio bulk ops, perf fixes, bug fixes, confirm dialog visibility
Confirm Dialog: - Overlay opacity 80% -> 95% (solid black) - Card uses bg-surface with glow shadow ring-primary/40 - Added AlertTriangle icon for danger variant - Removed backdrop-blur for maximum opacity Studio features: - Bulk delete: select imported items and delete in batch - Bulk nitro regen: select items missing .nitro and regenerate all - Added Trash2 and RefreshCw toolbar buttons for bulk actions Performance: - BatchProgress: React.memo + useMemo for entries/total/completed/percent - BatchProgress: ETA interval uses useRef to avoid re-creation per completed - CatalogRail: React.memo + useCallback for toggle - Removed redundant double setBatchProgress calls - Removed redundant new Map(initial) wrapping Bug fixes: - importBatch: stale batchDone closure always cleared progress panel (use ref) - importBatch: added missing toast.error on HTTP failure - bulkRegenNitro: now checks per-item results instead of marking all as succeeded - single deleteItem: now removes classname from selected set - retryFailed: clears progress panel on HTTP error and stream error - Removed unused 'done' variable (2x) from cloneAllMissing/retryFailed Lint: - biome format fixes for selectedMissingNitro and setItems
This commit is contained in:
1 parent
4863f78795
commit
93601e58e0
4 files changed
+214
-45
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -77,7 +78,7 @@ export function ConfirmDialog({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/95"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
if (!isLoading) onOpenChange(false);
|
||||
@@ -89,19 +90,26 @@ export function ConfirmDialog({
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-desc"
|
||||
className="relative z-10 w-full max-w-md rounded-lg border border-border bg-background/100 p-6 text-card-foreground shadow-2xl ring-1 ring-black/10 backdrop-blur-none"
|
||||
className="relative z-10 w-full max-w-md rounded-xl border-2 border-primary/40 bg-surface p-6 text-card-foreground shadow-[0_0_60px_-10px_rgba(245,158,11,0.3)]"
|
||||
variants={modalContentVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-left">
|
||||
<h2
|
||||
id="confirm-dialog-title"
|
||||
className="text-lg font-semibold leading-none"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{variant === "danger" && (
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-destructive/15">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
)}
|
||||
<h2
|
||||
id="confirm-dialog-title"
|
||||
className="text-lg font-semibold leading-none"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<p
|
||||
id="confirm-dialog-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Ban, Check, Loader2, ShieldCheck, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -40,7 +40,7 @@ function formatEta(ms: number): string {
|
||||
return `${min}m ${sec}s`;
|
||||
}
|
||||
|
||||
export function BatchProgress({
|
||||
export const BatchProgress = memo(function BatchProgress({
|
||||
progress,
|
||||
done,
|
||||
succeeded,
|
||||
@@ -65,33 +65,42 @@ export function BatchProgress({
|
||||
onDismiss?: () => void;
|
||||
onRetry?: (classnames: string[]) => 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;
|
||||
const { entries, total, completed, percent } = useMemo(() => {
|
||||
const e = [...progress.entries()];
|
||||
const t = e.length;
|
||||
const c = e.filter(
|
||||
([, s]) => s.status === "done" || s.status === "failed",
|
||||
).length;
|
||||
return {
|
||||
entries: e,
|
||||
total: t,
|
||||
completed: c,
|
||||
percent: t > 0 ? Math.round((c / t) * 100) : 0,
|
||||
};
|
||||
}, [progress]);
|
||||
|
||||
// Live ETA calculation
|
||||
const [eta, setEta] = useState<string | null>(null);
|
||||
const completedRef = useRef(completed);
|
||||
const totalRef = useRef(total);
|
||||
completedRef.current = completed;
|
||||
totalRef.current = total;
|
||||
|
||||
useEffect(() => {
|
||||
if (done || !startTime || completed === 0) {
|
||||
setEta(null);
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - startTime;
|
||||
const avgPerItem = elapsed / completed;
|
||||
const remaining = (total - completed) * avgPerItem;
|
||||
setEta(formatEta(remaining));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const calc = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const avgPerItem = elapsed / completed;
|
||||
const remaining = (total - completed) * avgPerItem;
|
||||
const avgPerItem = elapsed / completedRef.current;
|
||||
const remaining = (totalRef.current - completedRef.current) * avgPerItem;
|
||||
setEta(formatEta(remaining));
|
||||
}, 2000);
|
||||
};
|
||||
calc();
|
||||
const interval = setInterval(calc, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [done, startTime, completed, total]);
|
||||
}, [done, startTime, completed]);
|
||||
|
||||
return (
|
||||
<div className="border-t border-[var(--admin-border)] p-3">
|
||||
@@ -244,4 +253,4 @@ export function BatchProgress({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronRight, FolderTree } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { memo, useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TreeNode } from "@/types/catalog";
|
||||
|
||||
export function CatalogRail({
|
||||
export const CatalogRail = memo(function CatalogRail({
|
||||
tree,
|
||||
selectedId,
|
||||
onSelect,
|
||||
@@ -31,14 +31,14 @@ export function CatalogRail({
|
||||
return tree.filter((n) => !hidden.has(n.id));
|
||||
}, [tree, collapsed]);
|
||||
|
||||
const toggle = (id: number) => {
|
||||
const toggle = useCallback((id: number) => {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 w-64 shrink-0 flex-col border-r border-[var(--admin-border)] bg-[var(--admin-surface)]">
|
||||
@@ -100,4 +100,4 @@ export function CatalogRail({
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user