fix(admin): harden clothing/pets/effects/clone imports

Align grids on data.items, only treat SSE done as success, hard-fail
clothing sets when libs fail, and add Cancel via AbortController.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 15:56:38 +02:00
1 parent 9c4949186c
commit 16191cef14
8 files changed
+344 -173

No files matched your search

@@ -11,7 +11,7 @@ import {
Trash2,
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -103,6 +103,7 @@ function LibsView() {
total: number;
} | null>(null);
const [busyLib, setBusyLib] = useState<string | null>(null);
const batchAbortRef = useRef<AbortController | null>(null);
const fetchItems = useCallback(async (search: string, pageNum: number) => {
setLoading(true);
@@ -116,7 +117,7 @@ function LibsView() {
setError(data.error || "Failed to load");
return;
}
setItems(data.item || []);
setItems(data.items || []);
setTotal(data.meta?.total || 0);
setSelected(new Set());
} catch {
@@ -168,24 +169,41 @@ function LibsView() {
}
async function importViaSse(toImport: FigureItem[]) {
batchAbortRef.current?.abort();
const abort = new AbortController();
batchAbortRef.current = abort;
setBatchProgress({ done: 0, total: toImport.length });
let done = 0;
await runSseImport(
"/api/admin/import/clothing/batch",
{ items: toImport, concurrency: 3 },
(label) => {
done++;
markDone(label);
setBatchProgress({ done, total: toImport.length });
},
(succeeded, failed) => {
setBatchProgress(null);
setSelected(new Set());
if (succeeded > 0) toast.success(`${succeeded} libraries imported`);
if (failed > 0) toast.error(`${failed} failed`);
},
);
setBatchProgress(null);
try {
await runSseImport(
"/api/admin/import/clothing/batch",
{ items: toImport, concurrency: 3 },
(label) => {
done++;
markDone(label);
setBatchProgress({ done, total: toImport.length });
},
(succeeded, failed) => {
setSelected(new Set());
if (succeeded > 0) toast.success(`${succeeded} libraries imported`);
if (failed > 0) toast.error(`${failed} failed`);
},
abort.signal,
() => {
done++;
setBatchProgress({ done, total: toImport.length });
},
);
} catch (err) {
if ((err as Error)?.name === "AbortError") {
toast.info("Import cancelled");
} else {
throw err;
}
} finally {
if (batchAbortRef.current === abort) batchAbortRef.current = null;
setBatchProgress(null);
}
}
async function importOne(p: FigureItem) {
@@ -327,9 +345,19 @@ function LibsView() {
</Button>
)}
{batchProgress && (
<span className="text-xs text-muted-foreground">
{batchProgress.done}/{batchProgress.total}
</span>
<>
<span className="text-xs text-muted-foreground">
{batchProgress.done}/{batchProgress.total}
</span>
<Button
size="sm"
variant="outline"
className="h-8"
onClick={() => batchAbortRef.current?.abort()}
>
Cancel
</Button>
</>
)}
</div>
)}
@@ -476,6 +504,7 @@ function SetsView() {
total: number;
} | null>(null);
const [busyKey, setBusyKey] = useState<string | null>(null);
const batchAbortRef = useRef<AbortController | null>(null);
function itemKey(it: ClothingSetItem): string {
return `${it.setType}-${it.setId}`;
@@ -493,7 +522,7 @@ function SetsView() {
setError(data.error || "Failed to load");
return;
}
setItems(data.item || []);
setItems(data.items || []);
setTotal(data.meta?.total || 0);
setSelected(new Set());
} catch {
@@ -542,27 +571,47 @@ function SetsView() {
}
async function importViaSse(toImport: ClothingSetItem[]) {
batchAbortRef.current?.abort();
const abort = new AbortController();
batchAbortRef.current = abort;
setBatchProgress({ done: 0, total: toImport.length });
let done = 0;
await runSseImport(
"/api/admin/import/clothing/sets/batch",
{
items: toImport.map((it) => ({ setType: it.setType, setId: it.setId })),
concurrency: 3,
},
(label) => {
done++;
markDone(label);
setBatchProgress({ done, total: toImport.length });
},
(succeeded, failed) => {
setBatchProgress(null);
setSelected(new Set());
if (succeeded > 0) toast.success(`${succeeded} sets imported`);
if (failed > 0) toast.error(`${failed} failed`);
},
);
setBatchProgress(null);
try {
await runSseImport(
"/api/admin/import/clothing/sets/batch",
{
items: toImport.map((it) => ({
setType: it.setType,
setId: it.setId,
})),
concurrency: 3,
},
(label) => {
done++;
markDone(label);
setBatchProgress({ done, total: toImport.length });
},
(succeeded, failed) => {
setSelected(new Set());
if (succeeded > 0) toast.success(`${succeeded} sets imported`);
if (failed > 0) toast.error(`${failed} failed`);
},
abort.signal,
() => {
done++;
setBatchProgress({ done, total: toImport.length });
},
);
} catch (err) {
if ((err as Error)?.name === "AbortError") {
toast.info("Import cancelled");
} else {
throw err;
}
} finally {
if (batchAbortRef.current === abort) batchAbortRef.current = null;
setBatchProgress(null);
}
}
async function importOne(it: ClothingSetItem) {
@@ -684,9 +733,19 @@ function SetsView() {
</Button>
)}
{batchProgress && (
<span className="text-xs text-muted-foreground">
{batchProgress.done}/{batchProgress.total}
</span>
<>
<span className="text-xs text-muted-foreground">
{batchProgress.done}/{batchProgress.total}
</span>
<Button
size="sm"
variant="outline"
className="h-8"
onClick={() => batchAbortRef.current?.abort()}
>
Cancel
</Button>
</>
)}
</div>
)}
@@ -838,7 +897,7 @@ export function ImportClothingClient() {
className="h-7 text-xs"
onClick={() => setMode("libs")}
>
Librerie
Libraries
</Button>
<Button
size="sm"
@@ -846,7 +905,7 @@ export function ImportClothingClient() {
className="h-7 text-xs"
onClick={() => setMode("sets")}
>
Vestiti (set)
Sets
</Button>
</div>