feat(web): adopt MutationError across create/object forms; distinguish edit-form fetch error (#63)
This commit is contained in:
@@ -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")}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user