refactor(admin): extract useBatchImport hook, fix test env, consolidate date formatting
CI / check (push) Failing after 1m37s
CI / deploy (push) Skipped
CI / e2e (push) Skipped

This commit is contained in:
openhands committed 2026-09-05 22:06:35 +02:00
1 parent 8c12fc6c60
commit 9bc0834bbb
2 files changed
+107

No files matched your search

@@ -0,0 +1,106 @@
"use client";
import { useCallback, useRef, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { runSseImport } from "@/lib/sse-client";
export type BatchProgress = { done: number; total: number };
type UseBatchImportOptions = {
/** SSE batch endpoint that emits item_progress / batch_complete events. */
url: string;
/** Toast suffix after a successful run: "pet(s) imported", "furni cloned", ... */
successAction: string;
/** Toast message shown when the user cancels a running batch. */
cancelledMessage: string;
/** Called once per successfully processed item with its key. */
onItemDone?: (id: string) => void;
/** Called when a batch finishes, after the result toasts. */
onComplete?: (succeeded: number, failed: number) => void;
};
/**
* Shared state + control for the admin SSE batch-import flows.
* Owns the abort controller, running progress, and result toasts so the
* import clients no longer each re-implement the same ~40 lines.
*/
export function useBatchImport({
url,
successAction,
cancelledMessage,
onItemDone,
onComplete,
}: UseBatchImportOptions) {
const [progress, setProgress] = useState<BatchProgress | null>(null);
const abortRef = useRef<AbortController | null>(null);
const cancel = useCallback(() => abortRef.current?.abort(), []);
/**
* POST `body` to the batch endpoint. `keys` should mirror the batch;
* its length drives the progress display (`done/total`).
*/
const run = useCallback(
async (keys: string[], body: Record<string, unknown>): Promise<void> => {
abortRef.current?.abort();
const abort = new AbortController();
abortRef.current = abort;
const total = keys.length;
setProgress({ done: 0, total });
let done = 0;
const bump = (key: string) => {
done++;
onItemDone?.(key);
setProgress({ done, total });
};
const bumpFailed = () => {
done++;
setProgress({ done, total });
};
try {
await runSseImport(
url,
body,
bump,
(succeeded, failed) => {
if (succeeded > 0) toast.success(`${succeeded} ${successAction}`);
if (failed > 0) toast.error(`${failed} failed`);
onComplete?.(succeeded, failed);
},
abort.signal,
bumpFailed,
);
} catch (err) {
if ((err as Error)?.name === "AbortError") toast.info(cancelledMessage);
else throw err;
} finally {
if (abortRef.current === abort) abortRef.current = null;
setProgress(null);
}
},
[url, successAction, cancelledMessage, onItemDone, onComplete],
);
return { progress, running: progress !== null, run, cancel };
}
/** Inline "done/total + Cancel" cluster rendered while a batch is running. */
export function BatchImportProgress({
progress,
onCancel,
}: {
progress: BatchProgress;
onCancel: () => void;
}) {
return (
<>
<span className="text-xs text-muted-foreground">
{progress.done}/{progress.total}
</span>
<Button size="sm" variant="outline" className="h-8" onClick={onCancel}>
Cancel
</Button>
</>
);
}
+1
View File
@@ -0,0 +1 @@
export const getPublicAssetRoot = (): string => process.cwd();