metadataapi/tagapi/bulkeditapi als Backend-Voraussetzung. MetadataForm passt sich der Felddefinition an und warnt vor Verwerfen ungespeicherter Aenderungen, TagInput schlaegt Tags vor und ist per Pfeiltasten/Enter bedienbar, Massenbearbeitung wendet Tags auf beliebig grosse Auswahlen in einem Aufruf an. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
3.7 KiB
TypeScript
129 lines
3.7 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 { 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 [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 : "Speichern fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
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">Speichern</button>{" "}
|
|
<button type="button" onClick={handleDiscardClick} disabled={!isDirty}>
|
|
Verwerfen
|
|
</button>
|
|
|
|
{showDiscardConfirm && (
|
|
<div role="alertdialog" style={{ border: "1px solid #c00", padding: 8, marginTop: 8 }}>
|
|
<p>Ungespeicherte Änderungen gehen verloren. Wirklich verwerfen?</p>
|
|
<button type="button" onClick={confirmDiscard}>
|
|
Ja, verwerfen
|
|
</button>{" "}
|
|
<button type="button" onClick={() => setShowDiscardConfirm(false)}>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|