diff --git a/web/src/components/labelled-record-create-form.test.tsx b/web/src/components/labelled-record-create-form.test.tsx new file mode 100644 index 0000000..443979b --- /dev/null +++ b/web/src/components/labelled-record-create-form.test.tsx @@ -0,0 +1,28 @@ +import { expect, test, vi } from "vitest"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { renderApp } from "../test/render"; +import { LabelledRecordCreateForm } from "./labelled-record-create-form"; + +test("submitting with empty labels shows the required error and does not call onCreate", async () => { + const onCreate = vi.fn(); + renderApp( + , + ); + await userEvent.click(screen.getByRole("button", { name: /create/i })); + expect(screen.getByRole("alert")).toBeInTheDocument(); + expect(onCreate).not.toHaveBeenCalled(); +}); + +test("a valid submit calls onCreate and the reset clears the inputs", async () => { + const onCreate = vi.fn((_labels: unknown, _uri: unknown, reset: () => void) => reset()); + renderApp( + , + ); + const labelInput = screen.getByLabelText(/^label$/i) as HTMLInputElement; + await userEvent.type(labelInput, "Bronze"); + await userEvent.click(screen.getByRole("button", { name: /create/i })); + expect(onCreate).toHaveBeenCalled(); + expect((screen.getByLabelText(/^label$/i) as HTMLInputElement).value).toBe(""); +}); diff --git a/web/src/components/labelled-record-create-form.tsx b/web/src/components/labelled-record-create-form.tsx new file mode 100644 index 0000000..819c7f5 --- /dev/null +++ b/web/src/components/labelled-record-create-form.tsx @@ -0,0 +1,76 @@ +import { useId, useState, type FormEvent, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +import type { components } from "../api/schema"; +import { LabelEditor } from "./label-editor"; +import { MutationError } from "./mutation-error"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +type LabelInput = components["schemas"]["LabelInput"]; + +/** Create form for a labelled record (term/authority): single-language label + + * optional external URI, with required-label validation and a status-aware error. + * `onCreate` performs the mutation and is handed a `reset` to clear the inputs on success. */ +export function LabelledRecordCreateForm({ + heading, + submitLabel, + pending, + error, + onCreate, +}: { + heading: ReactNode; + submitLabel: string; + pending: boolean; + error: unknown; + onCreate: (labels: LabelInput[], uri: string | null, reset: () => void) => void; +}) { + const { t } = useTranslation(); + const uriId = useId(); + + const [labels, setLabels] = useState([]); + const [uri, setUri] = useState(""); + const [requiredError, setRequiredError] = useState(false); + + const onSubmit = (event: FormEvent) => { + event.preventDefault(); + + if (!labels.some((l) => l.label)) { + setRequiredError(true); + return; + } + + setRequiredError(false); + onCreate(labels, uri.trim() || null, () => { + setLabels([]); + setUri(""); + }); + }; + + return ( +
+
{heading}
+ +
+ + setUri(e.target.value)} + /> +
+ {requiredError && ( +

+ {t("form.required")} +

+ )} + + + + ); +}