refactor(admin): extract useBatchImport hook, fix test env, consolidate date formatting
This commit is contained in:
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const getPublicAssetRoot = (): string => process.cwd();
|
||||
Reference in new issue
Block a user