feat(web): reusable DeleteConfirmDialog with in-use handling + stories
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
import { expect, userEvent, fn, within } from 'storybook/test'
|
||||||
|
|
||||||
|
import { DeleteConfirmDialog } from './delete-confirm-dialog'
|
||||||
|
import { InUseError } from '../api/queries'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: DeleteConfirmDialog,
|
||||||
|
tags: ['ai-generated'],
|
||||||
|
} satisfies Meta<typeof DeleteConfirmDialog>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Confirms: Story = {
|
||||||
|
args: { description: 'Delete this term? This cannot be undone.', onConfirm: fn() },
|
||||||
|
play: async ({ canvas, args }) => {
|
||||||
|
await userEvent.click(canvas.getByRole('button', { name: 'Delete' }))
|
||||||
|
const confirm = await within(document.body).findByRole('button', { name: 'Delete' })
|
||||||
|
await userEvent.click(confirm)
|
||||||
|
await expect(args.onConfirm).toHaveBeenCalled()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowsInUse: Story = {
|
||||||
|
args: {
|
||||||
|
description: 'Delete this term? This cannot be undone.',
|
||||||
|
onConfirm: async () => { throw new InUseError(7) },
|
||||||
|
},
|
||||||
|
play: async ({ canvas }) => {
|
||||||
|
await userEvent.click(canvas.getByRole('button', { name: 'Delete' }))
|
||||||
|
const confirm = await within(document.body).findByRole('button', { name: 'Delete' })
|
||||||
|
await userEvent.click(confirm)
|
||||||
|
await expect(await within(document.body).findByRole('alert')).toHaveTextContent(/used by 7/i)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { InUseError } from "../api/queries";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogAction,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function DeleteConfirmDialog({
|
||||||
|
description,
|
||||||
|
onConfirm,
|
||||||
|
triggerLabel,
|
||||||
|
}: {
|
||||||
|
/** Confirmation prompt, e.g. t("actions.confirmDeleteTerm"). */
|
||||||
|
description: string;
|
||||||
|
/** Performs the delete; may throw InUseError to surface the in-use count. */
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
/** Optional override for the trigger button text (defaults to actions.delete). */
|
||||||
|
triggerLabel?: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
setMessage(null);
|
||||||
|
try {
|
||||||
|
await onConfirm();
|
||||||
|
} catch (err) {
|
||||||
|
// Keep the dialog open; show the blocking reason. Never let the rejected
|
||||||
|
// mutation escape as an unhandled rejection.
|
||||||
|
setMessage(err instanceof InUseError ? t("actions.inUse", { count: err.count }) : t("form.rejected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<AlertDialogTrigger
|
||||||
|
render={
|
||||||
|
<Button variant="ghost" size="sm" className="text-red-600">
|
||||||
|
{triggerLabel ?? t("actions.delete")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogTitle>{t("actions.delete")}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
|
{message && (
|
||||||
|
<p role="alert" className="text-sm text-red-600">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("form.cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={confirm}>{t("actions.delete")}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user