Compare commits

..

2 Commits

Author SHA1 Message Date
logaritmisk d0e3772c34 merge: LabelEditor preserves other-language labels on edit (#55)
CI / web (push) Has been cancelled
2026-06-07 14:41:22 +02:00
logaritmisk a9e6788b0b fix(web): LabelEditor preserves other-language labels on edit (#55)
Editing a term/authority/field that already had labels in other languages
silently replaced the whole multilingual set with one default-language entry.
onChange now keeps non-default-language entries; the editor shows only the
default-language label (no longer falling back to an other-language one, which
made the clear/edit path write the wrong language) and surfaces a hint when
other-language labels exist on the record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:37:25 +02:00
5 changed files with 89 additions and 13 deletions
+17 -2
View File
@@ -31,9 +31,9 @@ export const Empty: Story = {
}
export const Prefilled: Story = {
args: { value: [{ lang: 'en', label: 'Bronze' }] },
args: { value: [{ lang: 'sv', label: 'Brons' }] },
play: async ({ canvas }) => {
await expect(canvas.getByLabelText('Label')).toHaveValue('Bronze')
await expect(canvas.getByLabelText('Label')).toHaveValue('Brons')
},
}
@@ -44,3 +44,18 @@ export const Editing: Story = {
await expect(input).toHaveValue('Ceramic')
},
}
// An entry that already has labels in other languages (e.g. English) shows only the
// default-language label, with a hint that the other-language labels are preserved.
export const WithOtherLanguages: Story = {
args: {
value: [
{ lang: 'en', label: 'Bronze' },
{ lang: 'sv', label: 'Brons' },
],
},
play: async ({ canvas }) => {
await expect(canvas.getByLabelText('Label')).toHaveValue('Brons')
await expect(canvas.getByText(/other languages/i)).toBeInTheDocument()
},
}
+57 -3
View File
@@ -8,9 +8,23 @@ import type { components } from "../api/schema";
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); }} />;
function Harness({
initial = [],
onChange,
}: {
initial?: LabelInput[];
onChange?: (v: LabelInput[]) => void;
}) {
const [value, setValue] = useState<LabelInput[]>(initial);
return (
<LabelEditor
value={value}
onChange={(v) => {
setValue(v);
onChange?.(v);
}}
/>
);
}
test("emits a single label at the instance default language", async () => {
@@ -31,3 +45,43 @@ test("clearing the input emits an empty array", async () => {
await userEvent.clear(input);
await waitFor(() => expect(seen[seen.length - 1]).toEqual([]));
});
test("editing preserves labels in other languages", async () => {
const seen: LabelInput[][] = [];
renderApp(
<Harness
initial={[
{ lang: "en", label: "Bronze" },
{ lang: "sv", label: "Brons" },
]}
onChange={(v) => seen.push(v)}
/>,
);
const input = screen.getByLabelText(/^label$/i);
expect((input as HTMLInputElement).value).toBe("Brons");
await userEvent.clear(input);
await userEvent.type(input, "Brons (ny)");
await waitFor(() => {
expect(seen[seen.length - 1]).toEqual([
{ lang: "en", label: "Bronze" },
{ lang: "sv", label: "Brons (ny)" },
]);
});
});
test("shows a hint when the entry has labels in other languages", () => {
renderApp(
<Harness
initial={[
{ lang: "en", label: "Bronze" },
{ lang: "sv", label: "Brons" },
]}
/>,
);
expect(screen.getByText(/other languages/i)).toBeInTheDocument();
});
test("no hint when only the default-language label exists", () => {
renderApp(<Harness initial={[{ lang: "sv", label: "Brons" }]} />);
expect(screen.queryByText(/other languages/i)).not.toBeInTheDocument();
});
+13 -6
View File
@@ -7,9 +7,10 @@ import { Label } from "@/components/ui/label";
type LabelInput = components["schemas"]["LabelInput"];
/** 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. */
/** Single-language label editor. Authors one label at the instance default language.
* Editing only touches the default-language entry — labels in other languages on the
* same record are preserved (not collapsed), so editing a term/authority that already
* has e.g. an English label keeps it. */
export function LabelEditor({
value,
onChange,
@@ -20,16 +21,22 @@ export function LabelEditor({
const { t } = useTranslation();
const { default_language } = useConfig();
const current =
value.find((l) => l.lang === default_language)?.label ?? value[0]?.label ?? "";
const current = value.find((l) => l.lang === default_language)?.label ?? "";
const hasOtherLanguages = value.some((l) => l.lang !== default_language);
const set = (label: string) =>
onChange(label.trim() ? [{ lang: default_language, label }] : []);
onChange([
...value.filter((l) => l.lang !== default_language),
...(label.trim() ? [{ lang: default_language, label }] : []),
]);
return (
<div className="space-y-1">
<Label htmlFor="label">{t("labels.label")}</Label>
<Input id="label" value={current} onChange={(e) => set(e.target.value)} />
{hasOtherLanguages && (
<p className="text-xs text-muted-foreground">{t("labels.otherLanguages")}</p>
)}
</div>
);
}
+1 -1
View File
@@ -8,7 +8,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", "fieldRejected": "The field \"{{field}}\" was rejected — check its value", "flexibleHeading": "Catalogue fields" },
"actions": { "edit": "Edit", "delete": "Delete", "rename": "Rename", "save": "Save", "closeDetail": "Close detail", "confirmDelete": "Delete this object? This cannot be undone.", "confirmDeleteTerm": "Delete this term? This cannot be undone.", "confirmDeleteAuthority": "Delete this authority? This cannot be undone.", "confirmDeleteField": "Delete this field definition? This cannot be undone.", "confirmDeleteVocabulary": "Delete this vocabulary? This cannot be undone.", "inUse": "Can't delete — used by {{count}} object(s). Clear those fields first." },
"labels": { "label": "Label", "externalUri": "External URI (optional)" },
"labels": { "label": "Label", "externalUri": "External URI (optional)", "otherLanguages": "This entry also has labels in other languages, which are kept." },
"vocab": {
"newVocabulary": "New vocabulary", "key": "Key",
"create": "Create", "selectPrompt": "Select a vocabulary to manage its terms",
+1 -1
View File
@@ -8,7 +8,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", "fieldRejected": "Fältet \"{{field}}\" avvisades — kontrollera värdet", "flexibleHeading": "Katalogfält" },
"actions": { "edit": "Redigera", "delete": "Ta bort", "rename": "Byt namn", "save": "Spara", "closeDetail": "Stäng detalj", "confirmDelete": "Ta bort detta föremål? Detta kan inte ångras.", "confirmDeleteTerm": "Ta bort denna term? Detta kan inte ångras.", "confirmDeleteAuthority": "Ta bort denna auktoritet? Detta kan inte ångras.", "confirmDeleteField": "Ta bort denna fältdefinition? Detta kan inte ångras.", "confirmDeleteVocabulary": "Ta bort denna vokabulär? Detta kan inte ångras.", "inUse": "Kan inte ta bort — används av {{count}} föremål. Rensa de fälten först." },
"labels": { "label": "Etikett", "externalUri": "Extern URI (valfritt)" },
"labels": { "label": "Etikett", "externalUri": "Extern URI (valfritt)", "otherLanguages": "Denna post har även etiketter på andra språk, som behålls." },
"vocab": {
"newVocabulary": "Ny vokabulär", "key": "Nyckel",
"create": "Skapa", "selectPrompt": "Välj en vokabulär för att hantera dess termer",