feat(web): dynamic FieldInput (text/integer/date/boolean/localized_text/term/authority)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { components } from "../api/schema";
|
||||
import { useAuthorities, useTerms } from "../api/queries";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
type FieldDefinitionView = components["schemas"]["FieldDefinitionView"];
|
||||
type LabelView = components["schemas"]["LabelView"];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyForm = UseFormReturn<{ fields: Record<string, any> }>;
|
||||
|
||||
function labelIn(labels: LabelView[], lang: string): string {
|
||||
return (
|
||||
labels.find((l) => l.lang === lang)?.label ??
|
||||
labels.find((l) => l.lang === "en")?.label ??
|
||||
labels[0]?.label ??
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
// A native <select> keeps the bundle lean and is fully accessible; the shadcn Select
|
||||
// can replace it later without changing the value contract (option value = id).
|
||||
function OptionsSelect({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
lang,
|
||||
placeholder,
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
options: { id: string; labels: LabelView[] }[];
|
||||
lang: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
return (
|
||||
<select
|
||||
id={id}
|
||||
className="w-full rounded border px-2 py-1 text-sm"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
|
||||
{options.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{labelIn(o.labels, lang)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldInput({
|
||||
definition,
|
||||
form,
|
||||
}: {
|
||||
definition: FieldDefinitionView;
|
||||
form: AnyForm;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language.startsWith("sv") ? "sv" : "en";
|
||||
const label = labelIn(definition.labels, lang);
|
||||
const name = `fields.${definition.key}` as `fields.${string}`;
|
||||
const placeholder = t("form.selectPlaceholder");
|
||||
|
||||
switch (definition.data_type) {
|
||||
case "integer":
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
|
||||
<Input
|
||||
id={definition.key}
|
||||
type="number"
|
||||
{...form.register(name, {
|
||||
valueAsNumber: true,
|
||||
required: definition.required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
|
||||
<Input
|
||||
id={definition.key}
|
||||
type="date"
|
||||
{...form.register(name, { required: definition.required })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "boolean":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id={definition.key}
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(checked === true)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "localized_text":
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label>{label}</Label>
|
||||
|
||||
<Label
|
||||
htmlFor={`${definition.key}-en`}
|
||||
className="text-xs text-neutral-500"
|
||||
>
|
||||
{label} (EN)
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id={`${definition.key}-en`}
|
||||
{...form.register(`fields.${definition.key}.en` as `fields.${string}`)}
|
||||
/>
|
||||
|
||||
<Label
|
||||
htmlFor={`${definition.key}-sv`}
|
||||
className="text-xs text-neutral-500"
|
||||
>
|
||||
{label} (SV)
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id={`${definition.key}-sv`}
|
||||
{...form.register(`fields.${definition.key}.sv` as `fields.${string}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "term":
|
||||
return (
|
||||
<TermField
|
||||
definition={definition}
|
||||
form={form}
|
||||
label={label}
|
||||
lang={lang}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
|
||||
case "authority":
|
||||
return (
|
||||
<AuthorityField
|
||||
definition={definition}
|
||||
form={form}
|
||||
label={label}
|
||||
lang={lang}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
|
||||
case "text":
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
|
||||
<Input
|
||||
id={definition.key}
|
||||
{...form.register(name, { required: definition.required })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TermField({
|
||||
definition,
|
||||
form,
|
||||
label,
|
||||
lang,
|
||||
placeholder,
|
||||
}: {
|
||||
definition: FieldDefinitionView;
|
||||
form: AnyForm;
|
||||
label: string;
|
||||
lang: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const { data: terms } = useTerms(definition.vocabulary_id);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={`fields.${definition.key}` as `fields.${string}`}
|
||||
rules={{ required: definition.required }}
|
||||
render={({ field }) => (
|
||||
<OptionsSelect
|
||||
id={definition.key}
|
||||
value={(field.value as string) ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={terms ?? []}
|
||||
lang={lang}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthorityField({
|
||||
definition,
|
||||
form,
|
||||
label,
|
||||
lang,
|
||||
placeholder,
|
||||
}: {
|
||||
definition: FieldDefinitionView;
|
||||
form: AnyForm;
|
||||
label: string;
|
||||
lang: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const { data: authorities } = useAuthorities(definition.authority_kind);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={definition.key}>{label}</Label>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={`fields.${definition.key}` as `fields.${string}`}
|
||||
rules={{ required: definition.required }}
|
||||
render={({ field }) => (
|
||||
<OptionsSelect
|
||||
id={definition.key}
|
||||
value={(field.value as string) ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={authorities ?? []}
|
||||
lang={lang}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user