no_match resources (and lookup failures after auto-review gives up) now
open a decision in the inbox with options to set a TMDB ID, pick a local
work, or dismiss; ambiguous decisions get the same manual options.
Resolve accepts identity {tmdb | mediaEntityId} and assigns with a manual
lock. The inbox option dialog collects the TMDB type/ID or searches local
works; the identity pool links each pending item to its inbox filter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { LoaderCircle } from "lucide-react";
|
|
import { type ReactNode, useState } from "react";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "~/components/ui/alert-dialog.tsx";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
|
|
/**
|
|
* 确认弹窗。onConfirm 可返回 Promise:执行中按钮禁用并转圈,resolve 后自动关闭,reject 时保持打开
|
|
* (错误提示由调用方 toast)。受控(open/onOpenChange)或非受控(trigger)两种用法。
|
|
* <ConfirmDialog trigger={<Button variant="destructive">删除</Button>} title="删除实例?"
|
|
* description="…" confirmLabel="删除" destructive onConfirm={() => del.mutateAsync(id)} />
|
|
*/
|
|
export function ConfirmDialog({
|
|
trigger,
|
|
open: openProp,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
confirmLabel = "确认",
|
|
cancelLabel = "取消",
|
|
destructive = false,
|
|
confirmDisabled = false,
|
|
onConfirm,
|
|
children,
|
|
}: {
|
|
trigger?: ReactNode;
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
title: ReactNode;
|
|
description?: ReactNode;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
destructive?: boolean;
|
|
/** 输入未填完整时禁用确认按钮 */
|
|
confirmDisabled?: boolean;
|
|
onConfirm: () => unknown | Promise<unknown>;
|
|
/** 描述下方的额外内容(输入确认等) */
|
|
children?: ReactNode;
|
|
}) {
|
|
const [inner, setInner] = useState(false);
|
|
const [pending, setPending] = useState(false);
|
|
const open = openProp ?? inner;
|
|
const setOpen = (o: boolean) => {
|
|
if (pending) return;
|
|
setInner(o);
|
|
onOpenChange?.(o);
|
|
};
|
|
|
|
const run = async () => {
|
|
setPending(true);
|
|
try {
|
|
await onConfirm();
|
|
setPending(false);
|
|
setInner(false);
|
|
onOpenChange?.(false);
|
|
} catch {
|
|
setPending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
{trigger ? <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger> : null}
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
{description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}
|
|
</AlertDialogHeader>
|
|
{children}
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>
|
|
<Button variant={destructive ? "destructive" : "default"} onClick={run} disabled={pending || confirmDisabled}>
|
|
{pending ? <LoaderCircle className="animate-spin" /> : null}
|
|
{confirmLabel}
|
|
</Button>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|