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 type { components } from "../api/schema";
|
||||||
import { useAuthorities, useCreateAuthority } from "../api/queries";
|
import { useAuthorities, useCreateAuthority } from "../api/queries";
|
||||||
import { LabelEditor } from "../components/label-editor";
|
import { LabelEditor } from "../components/label-editor";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -141,11 +142,7 @@ export function AuthoritiesPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{create.isError && (
|
<MutationError error={create.error} />
|
||||||
<p role="alert" className="text-xs text-destructive">
|
|
||||||
{t("form.rejected")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button type="submit" size="sm" disabled={create.isPending}>
|
<Button type="submit" size="sm" disabled={create.isPending}>
|
||||||
{t("authorities.create")}
|
{t("authorities.create")}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
useVocabularies,
|
useVocabularies,
|
||||||
} from "../api/queries";
|
} from "../api/queries";
|
||||||
import { LabelEditor } from "../components/label-editor";
|
import { LabelEditor } from "../components/label-editor";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -95,7 +96,6 @@ export function FieldForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pending = isEdit ? update.isPending : create.isPending;
|
const pending = isEdit ? update.isPending : create.isPending;
|
||||||
const failed = isEdit ? update.isError : create.isError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="space-y-3 overflow-auto p-4">
|
<form onSubmit={onSubmit} className="space-y-3 overflow-auto p-4">
|
||||||
@@ -198,11 +198,7 @@ export function FieldForm({
|
|||||||
{t("form.required")}
|
{t("form.required")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{failed && (
|
<MutationError error={isEdit ? update.error : create.error} />
|
||||||
<p role="alert" className="text-xs text-destructive">
|
|
||||||
{t("form.rejected")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button type="submit" size="sm" disabled={pending}>
|
<Button type="submit" size="sm" disabled={pending}>
|
||||||
{isEdit ? t("actions.save") : t("fields.create")}
|
{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((putCore as { object_name: string }).object_name).toBe("Big amphora");
|
||||||
expect((putFields as { inscription: string }).inscription).toBe("old");
|
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 type { components } from "../api/schema";
|
||||||
import { useObject, useUpdateObject, useSetFields, FieldRejection } from "../api/queries";
|
import { useObject, useUpdateObject, useSetFields, FieldRejection } from "../api/queries";
|
||||||
|
import { errorMessageKey } from "../api/error-message";
|
||||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||||
import { ObjectForm, type ObjectCore, type ObjectFormValues } from "./object-form";
|
import { ObjectForm, type ObjectCore, type ObjectFormValues } from "./object-form";
|
||||||
import { FormSkeleton } from "@/components/ui/skeletons";
|
import { FormSkeleton } from "@/components/ui/skeletons";
|
||||||
@@ -14,10 +15,12 @@ export function ObjectEditForm() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const { data: object, isLoading } = useObject(id!);
|
const { data: object, isLoading, isError } = useObject(id!);
|
||||||
|
|
||||||
if (isLoading) return <FormSkeleton />;
|
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>;
|
if (!object) return <p className="p-4 text-sm text-muted-foreground">{t("objects.notFound")}</p>;
|
||||||
|
|
||||||
return <ObjectEditFormLoaded object={object} id={id!} />;
|
return <ObjectEditFormLoaded object={object} id={id!} />;
|
||||||
@@ -84,7 +87,8 @@ function ObjectEditFormLoaded({ object, id }: { object: AdminObjectView; id: str
|
|||||||
setFieldErrorCode(e.code);
|
setFieldErrorCode(e.code);
|
||||||
setError(t("form.fieldRejected", { field: e.field }));
|
setError(t("form.fieldRejected", { field: e.field }));
|
||||||
} else {
|
} else {
|
||||||
setError(t("form.rejected"));
|
const { key, opts } = errorMessageKey(e);
|
||||||
|
setError(t(key, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { ObjectForm, type ObjectFormValues } from "./object-form";
|
import { ObjectForm, type ObjectFormValues } from "./object-form";
|
||||||
import { useCreateObject, useSetFields, FieldRejection } from "../api/queries";
|
import { useCreateObject, useSetFields, FieldRejection } from "../api/queries";
|
||||||
|
import { errorMessageKey } from "../api/error-message";
|
||||||
import { useDocumentTitle } from "../lib/use-document-title";
|
import { useDocumentTitle } from "../lib/use-document-title";
|
||||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||||
import { PageTitle } from "@/components/ui/page-title";
|
import { PageTitle } from "@/components/ui/page-title";
|
||||||
@@ -33,8 +34,9 @@ export function ObjectNewPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
id = created.id;
|
id = created.id;
|
||||||
} catch {
|
} catch (e) {
|
||||||
setError(t("form.rejected"));
|
const { key, opts } = errorMessageKey(e);
|
||||||
|
setError(t(key, opts));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary } from "../api/queries";
|
import { useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary } from "../api/queries";
|
||||||
import { byKey } from "../lib/sort";
|
import { byKey } from "../lib/sort";
|
||||||
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -54,11 +55,7 @@ export function VocabularyList() {
|
|||||||
{t("vocab.create")}
|
{t("vocab.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{create.isError && (
|
<MutationError error={create.error} />
|
||||||
<p role="alert" className="text-xs text-destructive">
|
|
||||||
{t("form.rejected")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
<div className="border-b p-2">
|
<div className="border-b p-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -106,11 +103,7 @@ export function VocabularyList() {
|
|||||||
<Button type="button" variant="ghost" size="sm" onClick={() => setEditingId(null)}>
|
<Button type="button" variant="ghost" size="sm" onClick={() => setEditingId(null)}>
|
||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
{renameVocabulary.isError && (
|
<MutationError error={renameVocabulary.error} />
|
||||||
<p role="alert" className="text-xs text-destructive">
|
|
||||||
{t("form.rejected")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { byLabel } from "../lib/sort";
|
|||||||
import { labelText } from "../lib/labels";
|
import { labelText } from "../lib/labels";
|
||||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||||
import { LabelEditor } from "../components/label-editor";
|
import { LabelEditor } from "../components/label-editor";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { TermRow } from "./term-row";
|
import { TermRow } from "./term-row";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -116,11 +117,7 @@ export function VocabularyTerms() {
|
|||||||
{t("form.required")}
|
{t("form.required")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{addTerm.isError && (
|
<MutationError error={addTerm.error} />
|
||||||
<p role="alert" className="text-xs text-destructive">
|
|
||||||
{t("form.rejected")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<Button type="submit" size="sm" disabled={addTerm.isPending}>
|
<Button type="submit" size="sm" disabled={addTerm.isPending}>
|
||||||
{t("vocab.addTerm")}
|
{t("vocab.addTerm")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user