feat(web): single-language content authoring (LabelEditor + localized_text at default lang)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { expect, test } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderApp } from "../test/render";
|
||||
import { LabelEditor } from "./label-editor";
|
||||
@@ -10,28 +10,24 @@ type LabelInput = components["schemas"]["LabelInput"];
|
||||
|
||||
function Harness({ onChange }: { onChange: (v: LabelInput[]) => void }) {
|
||||
const [value, setValue] = useState<LabelInput[]>([]);
|
||||
return (
|
||||
<LabelEditor
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <LabelEditor value={value} onChange={(v) => { setValue(v); onChange(v); }} />;
|
||||
}
|
||||
|
||||
test("typing EN and SV emits both labels; empty langs are omitted", async () => {
|
||||
test("emits a single label at the instance default language", async () => {
|
||||
const seen: LabelInput[][] = [];
|
||||
renderApp(<Harness onChange={(v) => seen.push(v)} />);
|
||||
await userEvent.type(screen.getByLabelText(/label \(en\)/i), "Bronze");
|
||||
await userEvent.type(screen.getByLabelText(/label \(sv\)/i), "Brons");
|
||||
const last = seen[seen.length - 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);
|
||||
await userEvent.type(screen.getByLabelText(/^label$/i), "Brons");
|
||||
await waitFor(() => {
|
||||
const last = seen[seen.length - 1]!;
|
||||
expect(last).toEqual([{ lang: "sv", label: "Brons" }]);
|
||||
});
|
||||
});
|
||||
|
||||
test("clearing the input emits an empty array", async () => {
|
||||
const seen: LabelInput[][] = [];
|
||||
renderApp(<Harness onChange={(v) => seen.push(v)} />);
|
||||
const input = screen.getByLabelText(/^label$/i);
|
||||
await userEvent.type(input, "X");
|
||||
await userEvent.clear(input);
|
||||
await waitFor(() => expect(seen[seen.length - 1]).toEqual([]));
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { components } from "../api/schema";
|
||||
import { useConfig } from "../config/config-context";
|
||||
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. */
|
||||
/** Single-language label editor. Authors one label at the instance default language;
|
||||
* emits a one-entry LabelInput[] (empty array when blank). The multilingual data model
|
||||
* is unchanged — this only simplifies authoring. */
|
||||
export function LabelEditor({
|
||||
value,
|
||||
onChange,
|
||||
@@ -15,33 +18,18 @@ export function LabelEditor({
|
||||
onChange: (labels: LabelInput[]) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { default_language } = useConfig();
|
||||
|
||||
const valueFor = (lang: string) => value.find((l) => l.lang === lang)?.label ?? "";
|
||||
const current =
|
||||
value.find((l) => l.lang === default_language)?.label ?? value[0]?.label ?? "";
|
||||
|
||||
const set = (lang: string, label: string) => {
|
||||
const others = value.filter((l) => l.lang !== lang);
|
||||
|
||||
onChange(label.trim() ? [...others, { lang, label }] : others);
|
||||
};
|
||||
const set = (label: string) =>
|
||||
onChange(label.trim() ? [{ lang: default_language, label }] : []);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="label-en">{t("labels.en")}</Label>
|
||||
<Input
|
||||
id="label-en"
|
||||
value={valueFor("en")}
|
||||
onChange={(e) => set("en", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="label-sv">{t("labels.sv")}</Label>
|
||||
<Input
|
||||
id="label-sv"
|
||||
value={valueFor("sv")}
|
||||
onChange={(e) => set("sv", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="label">{t("labels.label")}</Label>
|
||||
<Input id="label" value={current} onChange={(e) => set(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user