diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts
index 5e727ea..b004e4f 100644
--- a/web/src/api/queries.ts
+++ b/web/src/api/queries.ts
@@ -441,7 +441,7 @@ export function useUpdateTerm() {
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: ["terms", vocabularyId] }),
- meta: { successMessage: "toast.saved" },
+ meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
@@ -518,7 +518,7 @@ export function useUpdateAuthority() {
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: ["authorities", kind] }),
- meta: { successMessage: "toast.saved" },
+ meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
diff --git a/web/src/authorities/authority-row.tsx b/web/src/authorities/authority-row.tsx
index a9a5f60..04695db 100644
--- a/web/src/authorities/authority-row.tsx
+++ b/web/src/authorities/authority-row.tsx
@@ -5,6 +5,7 @@ import type { components } from "../api/schema";
import { useUpdateAuthority, useDeleteAuthority } from "../api/queries";
import { LabelEditor } from "../components/label-editor";
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
+import { MutationError } from "../components/mutation-error";
import { ExternalUriLink } from "../components/external-uri-link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -56,6 +57,7 @@ export function AuthorityRow({ authority, kind, lang }: { authority: AuthorityVi
{t("form.cancel")}
+
);
}
@@ -71,6 +73,7 @@ export function AuthorityRow({ authority, kind, lang }: { authority: AuthorityVi
variant="ghost"
size="sm"
onClick={() => {
+ updateAuthority.reset();
setLabels(authority.labels as LabelInput[]);
setUri(authority.external_uri ?? "");
setEditing(true);
diff --git a/web/src/components/delete-confirm-dialog.tsx b/web/src/components/delete-confirm-dialog.tsx
index 43dcd62..5792b1f 100644
--- a/web/src/components/delete-confirm-dialog.tsx
+++ b/web/src/components/delete-confirm-dialog.tsx
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { InUseError } from "../api/queries";
+import { errorMessageKey } from "../api/error-message";
import {
AlertDialog,
AlertDialogTrigger,
@@ -37,7 +37,8 @@ export function DeleteConfirmDialog({
} 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"));
+ const { key, opts } = errorMessageKey(err);
+ setMessage(t(key, opts));
return;
}
setOpen(false);
diff --git a/web/src/vocab/term-row.test.tsx b/web/src/vocab/term-row.test.tsx
new file mode 100644
index 0000000..8f99615
--- /dev/null
+++ b/web/src/vocab/term-row.test.tsx
@@ -0,0 +1,51 @@
+import { expect, test } from "vitest";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { http, HttpResponse } from "msw";
+
+import type { TermView } from "../test/fixtures";
+import { server } from "../test/server";
+import { renderApp } from "../test/render";
+import { TermRow } from "./term-row";
+
+const term: TermView = {
+ id: "t1",
+ external_uri: null,
+ labels: [{ lang: "en", label: "Bronze" }],
+};
+
+test("a failed term update shows an inline error and keeps the row editable", async () => {
+ server.use(
+ http.patch("/api/admin/vocabularies/:id/terms/:term_id", () => new HttpResponse(null, { status: 403 })),
+ );
+ renderApp(
+
,
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: /edit/i }));
+ await userEvent.click(screen.getByRole("button", { name: /save/i }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(/permission/i);
+ expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument();
+});
+
+test("re-entering edit after a failure clears the stale error", async () => {
+ server.use(
+ http.patch("/api/admin/vocabularies/:id/terms/:term_id", () => new HttpResponse(null, { status: 403 })),
+ );
+ renderApp(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: /edit/i }));
+ await userEvent.click(screen.getByRole("button", { name: /save/i }));
+ expect(await screen.findByRole("alert")).toBeInTheDocument();
+ await userEvent.click(screen.getByRole("button", { name: /cancel/i }));
+ await userEvent.click(screen.getByRole("button", { name: /edit/i }));
+
+ expect(screen.queryByRole("alert")).toBeNull();
+});
diff --git a/web/src/vocab/term-row.tsx b/web/src/vocab/term-row.tsx
index d1b5cd6..69261e1 100644
--- a/web/src/vocab/term-row.tsx
+++ b/web/src/vocab/term-row.tsx
@@ -5,6 +5,7 @@ import type { components } from "../api/schema";
import { useUpdateTerm, useDeleteTerm } from "../api/queries";
import { LabelEditor } from "../components/label-editor";
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
+import { MutationError } from "../components/mutation-error";
import { ExternalUriLink } from "../components/external-uri-link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -50,6 +51,7 @@ export function TermRow({ vocabularyId, term, lang }: { vocabularyId: string; te
{t("form.cancel")}
+
);
}
@@ -65,6 +67,7 @@ export function TermRow({ vocabularyId, term, lang }: { vocabularyId: string; te
variant="ghost"
size="sm"
onClick={() => {
+ updateTerm.reset();
setLabels(term.labels as LabelInput[]);
setUri(term.external_uri ?? "");
setEditing(true);