feat(web): adopt MutationError across create/object forms; distinguish edit-form fetch error (#63)

This commit is contained in:
2026-06-08 17:32:36 +02:00
parent 6e02ac874f
commit aeb1b084d9
7 changed files with 31 additions and 30 deletions
+2 -5
View File
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import type { components } from "../api/schema";
import { useAuthorities, useCreateAuthority } from "../api/queries";
import { LabelEditor } from "../components/label-editor";
import { MutationError } from "../components/mutation-error";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -141,11 +142,7 @@ export function AuthoritiesPage() {
</p>
)}
{create.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
<MutationError error={create.error} />
<Button type="submit" size="sm" disabled={create.isPending}>
{t("authorities.create")}
+2 -6
View File
@@ -8,6 +8,7 @@ import {
useVocabularies,
} from "../api/queries";
import { LabelEditor } from "../components/label-editor";
import { MutationError } from "../components/mutation-error";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -95,7 +96,6 @@ export function FieldForm({
};
const pending = isEdit ? update.isPending : create.isPending;
const failed = isEdit ? update.isError : create.isError;
return (
<form onSubmit={onSubmit} className="space-y-3 overflow-auto p-4">
@@ -198,11 +198,7 @@ export function FieldForm({
{t("form.required")}
</p>
)}
{failed && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
<MutationError error={isEdit ? update.error : create.error} />
<Button type="submit" size="sm" disabled={pending}>
{isEdit ? t("actions.save") : t("fields.create")}
+12
View File
@@ -69,3 +69,15 @@ test("edit: prefilled, save -> PUT core + PUT fields -> back to detail", async (
expect((putCore as { object_name: string }).object_name).toBe("Big amphora");
expect((putFields as { inscription: string }).inscription).toBe("old");
});
test("renders a load error (not 'not found') when the object fetch fails", async () => {
server.use(http.get("/api/admin/objects/:id", () => new HttpResponse(null, { status: 500 })));
renderApp(
<Routes>
<Route path="/objects/:id/edit" element={<ObjectEditForm />} />
</Routes>,
{ route: "/objects/abc/edit" },
);
expect(await screen.findByText(/could not load/i)).toBeInTheDocument();
expect(screen.queryByText(/not found/i)).toBeNull();
});
+6 -2
View File
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import type { components } from "../api/schema";
import { useObject, useUpdateObject, useSetFields, FieldRejection } from "../api/queries";
import { errorMessageKey } from "../api/error-message";
import { useBreadcrumb } from "../shell/use-breadcrumb";
import { ObjectForm, type ObjectCore, type ObjectFormValues } from "./object-form";
import { FormSkeleton } from "@/components/ui/skeletons";
@@ -14,10 +15,12 @@ export function ObjectEditForm() {
const { t } = useTranslation();
const { id } = useParams();
const { data: object, isLoading } = useObject(id!);
const { data: object, isLoading, isError } = useObject(id!);
if (isLoading) return <FormSkeleton />;
if (isError) return <p className="p-4 text-sm text-destructive">{t("objects.loadError")}</p>;
if (!object) return <p className="p-4 text-sm text-muted-foreground">{t("objects.notFound")}</p>;
return <ObjectEditFormLoaded object={object} id={id!} />;
@@ -84,7 +87,8 @@ function ObjectEditFormLoaded({ object, id }: { object: AdminObjectView; id: str
setFieldErrorCode(e.code);
setError(t("form.fieldRejected", { field: e.field }));
} else {
setError(t("form.rejected"));
const { key, opts } = errorMessageKey(e);
setError(t(key, opts));
}
return false;
+4 -2
View File
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { ObjectForm, type ObjectFormValues } from "./object-form";
import { useCreateObject, useSetFields, FieldRejection } from "../api/queries";
import { errorMessageKey } from "../api/error-message";
import { useDocumentTitle } from "../lib/use-document-title";
import { useBreadcrumb } from "../shell/use-breadcrumb";
import { PageTitle } from "@/components/ui/page-title";
@@ -33,8 +34,9 @@ export function ObjectNewPage() {
});
id = created.id;
} catch {
setError(t("form.rejected"));
} catch (e) {
const { key, opts } = errorMessageKey(e);
setError(t(key, opts));
return false;
}
+3 -10
View File
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary } from "../api/queries";
import { byKey } from "../lib/sort";
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
import { MutationError } from "../components/mutation-error";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -54,11 +55,7 @@ export function VocabularyList() {
{t("vocab.create")}
</Button>
</div>
{create.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
<MutationError error={create.error} />
</form>
<div className="border-b p-2">
<Input
@@ -106,11 +103,7 @@ export function VocabularyList() {
<Button type="button" variant="ghost" size="sm" onClick={() => setEditingId(null)}>
{t("form.cancel")}
</Button>
{renameVocabulary.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
<MutationError error={renameVocabulary.error} />
</form>
) : (
<>
+2 -5
View File
@@ -8,6 +8,7 @@ import { byLabel } from "../lib/sort";
import { labelText } from "../lib/labels";
import { useBreadcrumb } from "../shell/use-breadcrumb";
import { LabelEditor } from "../components/label-editor";
import { MutationError } from "../components/mutation-error";
import { TermRow } from "./term-row";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -116,11 +117,7 @@ export function VocabularyTerms() {
{t("form.required")}
</p>
)}
{addTerm.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
<MutationError error={addTerm.error} />
<Button type="submit" size="sm" disabled={addTerm.isPending}>
{t("vocab.addTerm")}
</Button>