diff --git a/web/src/components/label-editor.test.tsx b/web/src/components/label-editor.test.tsx new file mode 100644 index 0000000..47d22c9 --- /dev/null +++ b/web/src/components/label-editor.test.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { expect, test } from "vitest"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { renderApp } from "../test/render"; +import { LabelEditor } from "./label-editor"; +import type { components } from "../api/schema"; + +type LabelInput = components["schemas"]["LabelInput"]; + +function Harness({ onChange }: { onChange: (v: LabelInput[]) => void }) { + const [value, setValue] = useState([]); + return ( + { + setValue(v); + onChange(v); + }} + /> + ); +} + +test("typing EN and SV emits both labels; empty langs are omitted", async () => { + const seen: LabelInput[][] = []; + renderApp( seen.push(v)} />); + await userEvent.type(screen.getByLabelText(/label \(en\)/i), "Bronze"); + await userEvent.type(screen.getByLabelText(/label \(sv\)/i), "Brons"); + const last = seen.at(-1)!; + expect(last).toEqual( + expect.arrayContaining([ + { lang: "en", label: "Bronze" }, + { lang: "sv", label: "Brons" }, + ]), + ); + expect(seen.some((v) => v.length === 1 && v[0].lang === "en")).toBe(true); +}); diff --git a/web/src/components/label-editor.tsx b/web/src/components/label-editor.tsx new file mode 100644 index 0000000..3636382 --- /dev/null +++ b/web/src/components/label-editor.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from "react-i18next"; + +import type { components } from "../api/schema"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +type LabelInput = components["schemas"]["LabelInput"]; + +/** Controlled sv/en label editor. Emits LabelInput[] with only the non-empty langs. */ +export function LabelEditor({ + value, + onChange, +}: { + value: LabelInput[]; + onChange: (labels: LabelInput[]) => void; +}) { + const { t } = useTranslation(); + + const valueFor = (lang: string) => value.find((l) => l.lang === lang)?.label ?? ""; + + const set = (lang: string, label: string) => { + const others = value.filter((l) => l.lang !== lang); + + onChange(label ? [...others, { lang, label }] : others); + }; + + return ( +
+
+ + set("en", e.target.value)} + /> +
+
+ + set("sv", e.target.value)} + /> +
+
+ ); +} diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 21f404a..fed225e 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -7,6 +7,7 @@ "visibility": { "draft": "Draft", "internal": "Internal", "public": "Public" }, "form": { "selectPlaceholder": "— select —", "create": "Create object", "save": "Save", "cancel": "Cancel", "visibility": "Visibility", "draft": "Draft", "internal": "Internal", "required": "This field is required", "rejected": "The server rejected the changes — check required and referenced fields", "flexibleHeading": "Catalogue fields" }, "actions": { "edit": "Edit", "delete": "Delete", "confirmDelete": "Delete this object? This cannot be undone." }, + "labels": { "en": "Label (EN)", "sv": "Label (SV)", "externalUri": "External URI (optional)" }, "publish": { "heading": "Visibility", "advanceInternal": "Advance to internal", diff --git a/web/src/i18n/sv.json b/web/src/i18n/sv.json index 93e15b8..87427be 100644 --- a/web/src/i18n/sv.json +++ b/web/src/i18n/sv.json @@ -7,6 +7,7 @@ "visibility": { "draft": "Utkast", "internal": "Intern", "public": "Publik" }, "form": { "selectPlaceholder": "— välj —", "create": "Skapa föremål", "save": "Spara", "cancel": "Avbryt", "visibility": "Synlighet", "draft": "Utkast", "internal": "Intern", "required": "Fältet är obligatoriskt", "rejected": "Servern avvisade ändringarna — kontrollera obligatoriska och refererade fält", "flexibleHeading": "Katalogfält" }, "actions": { "edit": "Redigera", "delete": "Ta bort", "confirmDelete": "Ta bort detta föremål? Detta kan inte ångras." }, + "labels": { "en": "Etikett (EN)", "sv": "Etikett (SV)", "externalUri": "Extern URI (valfritt)" }, "publish": { "heading": "Synlighet", "advanceInternal": "Gör intern", diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json index cc58243..97295a4 100644 --- a/web/tsconfig.app.json +++ b/web/tsconfig.app.json @@ -1,9 +1,9 @@ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true,