From 5087e342806decf92d006ea144071bd5e8001d1b Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Thu, 4 Jun 2026 01:00:57 +0200 Subject: [PATCH] feat(web): delete object with confirm dialog Co-Authored-By: Claude Sonnet 4.6 --- web/src/i18n/en.json | 2 +- web/src/i18n/sv.json | 2 +- web/src/objects/delete-object-dialog.test.tsx | 64 +++++++++++++++++++ web/src/objects/delete-object-dialog.tsx | 51 +++++++++++++++ web/src/objects/object-detail.tsx | 2 + 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 web/src/objects/delete-object-dialog.test.tsx create mode 100644 web/src/objects/delete-object-dialog.tsx diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 5ddefbe..5cecee0 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -6,5 +6,5 @@ "fieldsLabels": { "objectNumber": "Object number", "objectName": "Name", "count": "Number of objects", "briefDescription": "Brief description", "currentLocation": "Current location", "currentOwner": "Current owner", "recorder": "Recorder", "recordingDate": "Recording date", "visibility": "Visibility", "flexible": "Catalogue fields" }, "visibility": { "draft": "Draft", "internal": "Internal", "public": "Public" }, "form": { "selectPlaceholder": "— select —", "create": "Create object", "save": "Save", "cancel": "Cancel", "visibility": "Visibility", "draft": "Draft", "internal": "Internal", "required": "This field is required", "rejected": "The server rejected the changes — check required and referenced fields", "flexibleHeading": "Catalogue fields" }, - "actions": { "edit": "Edit" } + "actions": { "edit": "Edit", "delete": "Delete", "confirmDelete": "Delete this object? This cannot be undone." } } diff --git a/web/src/i18n/sv.json b/web/src/i18n/sv.json index fe7a8f2..aa7ff7b 100644 --- a/web/src/i18n/sv.json +++ b/web/src/i18n/sv.json @@ -6,5 +6,5 @@ "fieldsLabels": { "objectNumber": "Föremålsnummer", "objectName": "Namn", "count": "Antal föremål", "briefDescription": "Kort beskrivning", "currentLocation": "Nuvarande plats", "currentOwner": "Nuvarande ägare", "recorder": "Registrerad av", "recordingDate": "Registreringsdatum", "visibility": "Synlighet", "flexible": "Katalogfält" }, "visibility": { "draft": "Utkast", "internal": "Intern", "public": "Publik" }, "form": { "selectPlaceholder": "— välj —", "create": "Skapa föremål", "save": "Spara", "cancel": "Avbryt", "visibility": "Synlighet", "draft": "Utkast", "internal": "Intern", "required": "Fältet är obligatoriskt", "rejected": "Servern avvisade ändringarna — kontrollera obligatoriska och refererade fält", "flexibleHeading": "Katalogfält" }, - "actions": { "edit": "Redigera" } + "actions": { "edit": "Redigera", "delete": "Ta bort", "confirmDelete": "Ta bort detta föremål? Detta kan inte ångras." } } diff --git a/web/src/objects/delete-object-dialog.test.tsx b/web/src/objects/delete-object-dialog.test.tsx new file mode 100644 index 0000000..75f3645 --- /dev/null +++ b/web/src/objects/delete-object-dialog.test.tsx @@ -0,0 +1,64 @@ +import { expect, test } from "vitest"; +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { Routes, Route } from "react-router-dom"; +import { server } from "../test/server"; +import { renderApp } from "../test/render"; +import { DeleteObjectDialog } from "./delete-object-dialog"; + +function tree() { + return ( + + } /> + objects list} /> + + ); +} + +test("confirm delete: DELETE then navigate to the list", async () => { + let deleted = false; + + server.use( + http.delete("/api/admin/objects/:id", () => { + deleted = true; + return new HttpResponse(null, { status: 204 }); + }), + ); + + renderApp(tree(), { route: "/objects/o-1" }); + + // open the dialog via the trigger + await userEvent.click(await screen.findByRole("button", { name: /delete/i })); + + // confirm inside the dialog + const dialog = await screen.findByRole("alertdialog"); + + await userEvent.click(within(dialog).getByRole("button", { name: /delete/i })); + + await waitFor(() => expect(screen.getByText("objects list")).toBeInTheDocument()); + + expect(deleted).toBe(true); +}); + +test("cancel does not delete", async () => { + let deleted = false; + + server.use( + http.delete("/api/admin/objects/:id", () => { + deleted = true; + return new HttpResponse(null, { status: 204 }); + }), + ); + + renderApp(tree(), { route: "/objects/o-1" }); + + await userEvent.click(await screen.findByRole("button", { name: /delete/i })); + + const dialog = await screen.findByRole("alertdialog"); + + await userEvent.click(within(dialog).getByRole("button", { name: /cancel/i })); + + expect(deleted).toBe(false); + expect(screen.queryByText("objects list")).not.toBeInTheDocument(); +}); diff --git a/web/src/objects/delete-object-dialog.tsx b/web/src/objects/delete-object-dialog.tsx new file mode 100644 index 0000000..ce97f9e --- /dev/null +++ b/web/src/objects/delete-object-dialog.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import { useDeleteObject } 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 DeleteObjectDialog({ id }: { id: string }) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const del = useDeleteObject(); + const [open, setOpen] = useState(false); + + const onConfirm = async () => { + await del.mutateAsync(id); + navigate("/objects"); + }; + + return ( + + + {t("actions.delete")} + + } + /> + + + {t("actions.delete")} + {t("actions.confirmDelete")} + + {t("form.cancel")} + + {t("actions.delete")} + + + + + ); +} diff --git a/web/src/objects/object-detail.tsx b/web/src/objects/object-detail.tsx index b69cb5b..15c3ba6 100644 --- a/web/src/objects/object-detail.tsx +++ b/web/src/objects/object-detail.tsx @@ -2,6 +2,7 @@ import { Link, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useObject, useFieldDefinitions } from "../api/queries"; +import { DeleteObjectDialog } from "./delete-object-dialog"; import { VisibilityBadge } from "./visibility-badge"; import { Skeleton } from "@/components/ui/skeleton"; @@ -60,6 +61,7 @@ export function ObjectDetail() { {t("actions.edit")} +