Files
nexarch/web/dms-browser/components/MetadataForm.tsx
patrickandClaude Sonnet 5 dd8c620e3c UX-04: Übersetzungen Deutsch und Englisch für dms-browser/pdf-viewer
Rueckwirkende Umstellung aller vor UX-03 gebauten dms-browser- und
pdf-viewer-Seiten/Komponenten auf die @nexarch/shl-i18n-Infrastruktur
(useI18n/t(), registerMessages, formatDate) statt hartkodierter
deutscher Texte. Schliesst die in UX-03 explizit offen gelassene
Retrofit-Luecke.

Neue Sprachdateien lib/messages.ts (dms-browser, pdf-viewer) mit
vollstaendigen DE/EN-Woerterbuechern. Neuer automatisierter
Abdeckungstest (i18n-coverage.test.ts, gleiche Heuristik wie in
@nexarch/shl) belegt: keine fest verdrahteten JSX-Texte mehr in
app/ bzw. components/ beider Apps.

web/shl/i18n/i18n.tsx splittet registry/registerMessages weiter in
i18n/messages.ts (kein "use client"), um die bekannte RSC-Client/
Server-Grenzverletzung zu vermeiden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 02:04:40 +02:00

132 lines
3.8 KiB
TypeScript

"use client";
// MET-03: Formular, das sich automatisch an die Felddefinition anpasst
// (Akzeptanzkriterium 1) — ein Eingabeelement je FieldDTO, Typ bestimmt den
// input-Typ. Jedes Feld hat ein <label htmlFor>, damit das Formular
// vollständig per Tastatur ausfüllbar ist (Akzeptanzkriterium/
// Pflichtprüfung 2) — Standard-HTML-Formularelemente sind das ohnehin,
// entscheidend ist nur, keine maus-exklusiven Interaktionen einzubauen.
import { useEffect, useState } from "react";
import { useI18n } from "@nexarch/shl";
import "../lib/messages";
import { FieldDTO } from "../lib/metadataApi";
function inputType(fieldType: FieldDTO["type"]): string {
switch (fieldType) {
case "number":
return "number";
case "date":
return "date";
case "bool":
return "checkbox";
default:
return "text";
}
}
export function MetadataForm({
fields,
initialValues,
onSave,
}: {
fields: FieldDTO[];
initialValues: Record<string, string>;
onSave: (values: Record<string, string>) => Promise<void>;
}) {
const { t } = useI18n();
const [values, setValues] = useState<Record<string, string>>(initialValues);
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => setValues(initialValues), [initialValues]);
const isDirty = fields.some((f) => (values[f.id] ?? "") !== (initialValues[f.id] ?? ""));
useEffect(() => {
if (!isDirty) return;
const handler = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = "";
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [isDirty]);
function setValue(fieldId: string, value: string) {
setValues((prev) => ({ ...prev, [fieldId]: value }));
}
async function handleSave() {
setError(null);
try {
await onSave(values);
} catch (err) {
setError(err instanceof Error ? err.message : t("dms.metadata.saveError"));
}
}
function handleDiscardClick() {
if (isDirty) {
setShowDiscardConfirm(true);
return;
}
}
function confirmDiscard() {
setValues(initialValues);
setShowDiscardConfirm(false);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
{error && <p role="alert">{error}</p>}
{fields.map((field) => (
<div key={field.id} style={{ marginBottom: 8 }}>
<label htmlFor={`field-${field.id}`}>
{field.label}
{field.required ? " *" : ""}
</label>
<br />
{field.type === "bool" ? (
<input
id={`field-${field.id}`}
type="checkbox"
checked={values[field.id] === "true"}
onChange={(e) => setValue(field.id, e.target.checked ? "true" : "false")}
/>
) : (
<input
id={`field-${field.id}`}
type={inputType(field.type)}
value={values[field.id] ?? ""}
onChange={(e) => setValue(field.id, e.target.value)}
/>
)}
</div>
))}
<button type="submit">{t("dms.metadata.save")}</button>{" "}
<button type="button" onClick={handleDiscardClick} disabled={!isDirty}>
{t("dms.metadata.discard")}
</button>
{showDiscardConfirm && (
<div role="alertdialog" style={{ border: "1px solid #c00", padding: 8, marginTop: 8 }}>
<p>{t("dms.metadata.confirmDiscard")}</p>
<button type="button" onClick={confirmDiscard}>
{t("dms.metadata.confirmYes")}
</button>{" "}
<button type="button" onClick={() => setShowDiscardConfirm(false)}>
{t("dms.metadata.confirmCancel")}
</button>
</div>
)}
</form>
);
}