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")}
+