Files
UHD_Media_Tool/apps/web/app/components/console/confirm-dialog.tsx
T
rootandClaude Opus 5.5 fb7727cf66 feat(identity): open identity_unresolved decisions for resources with no candidates
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]>
2026-09-28 13:44:57 +08:00

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>
);
}