2026-10-09 18:03:56 +00:00
|
|
|
import { Button } from "@repo/shadcn-ui/components/button";
|
|
|
|
|
import { ArrowDownIcon, ArrowUpIcon, Trash2Icon } from "lucide-react";
|
|
|
|
|
import { useId } from "react";
|
|
|
|
|
|
|
|
|
|
import type { FieldSection } from "./field-sections";
|
|
|
|
|
import { SelectWithAction } from "./select-with-action";
|
|
|
|
|
import { SwitchField } from "./switch-field";
|
|
|
|
|
import type { SectionBinding, SectionEditorLabels } from "./template-sections-editor";
|
|
|
|
|
|
|
|
|
|
export function TemplateSectionFields({
|
|
|
|
|
sectionId,
|
|
|
|
|
sections,
|
|
|
|
|
fields,
|
|
|
|
|
fieldNames,
|
|
|
|
|
fieldNotes,
|
|
|
|
|
labels,
|
|
|
|
|
disabled,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
sectionId?: string | undefined;
|
|
|
|
|
sections: readonly FieldSection[];
|
|
|
|
|
fields: readonly SectionBinding[];
|
|
|
|
|
fieldNames: Readonly<Record<string, string>>;
|
|
|
|
|
fieldNotes?: Readonly<Record<string, string>> | undefined;
|
|
|
|
|
labels: SectionEditorLabels;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
onChange: (fields: SectionBinding[]) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const prefix = useId();
|
|
|
|
|
const grouped = fields.filter((field) => field.sectionId === sectionId);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
const move = (fieldId: string, offset: number) => {
|
|
|
|
|
const position = grouped.findIndex((field) => field.fieldId === fieldId);
|
|
|
|
|
const neighbor = grouped[position + offset];
|
|
|
|
|
const current = grouped[position];
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
if (!(neighbor && current)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const next = [...fields];
|
|
|
|
|
const index = fields.indexOf(current);
|
|
|
|
|
const other = fields.indexOf(neighbor);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
next[index] = neighbor;
|
|
|
|
|
next[other] = current;
|
|
|
|
|
onChange(next);
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
if (!(sectionId || grouped.length)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{!sectionId && sections.length > 0 && (
|
|
|
|
|
<h4 className="text-sm font-medium">{labels.ungrouped}</h4>
|
|
|
|
|
)}
|
|
|
|
|
{!grouped.length && <p className="text-xs text-muted-foreground">{labels.empty}</p>}
|
|
|
|
|
{grouped.map((field, index) => {
|
|
|
|
|
const name = fieldNames[field.fieldId] ?? field.fieldId;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-09 18:03:56 +00:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-2 rounded-md border p-3" key={field.fieldId}>
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="break-words text-sm font-medium">{name}</p>
|
|
|
|
|
{fieldNotes?.[field.fieldId] && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">{fieldNotes[field.fieldId]}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex shrink-0 items-center">
|
|
|
|
|
<Button
|
|
|
|
|
aria-label={`${labels.moveUp}: ${name}`}
|
|
|
|
|
disabled={disabled || index === 0}
|
|
|
|
|
onClick={() => move(field.fieldId, -1)}
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
>
|
|
|
|
|
<ArrowUpIcon />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
aria-label={`${labels.moveDown}: ${name}`}
|
|
|
|
|
disabled={disabled || index === grouped.length - 1}
|
|
|
|
|
onClick={() => move(field.fieldId, 1)}
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
>
|
|
|
|
|
<ArrowDownIcon />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
aria-label={`${labels.removeField}: ${name}`}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={() => onChange(fields.filter((item) => item.fieldId !== field.fieldId))}
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
>
|
|
|
|
|
<Trash2Icon />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
|
|
|
<SwitchField
|
|
|
|
|
checked={field.required}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
id={`${prefix}-${field.fieldId}-required`}
|
|
|
|
|
label={
|
|
|
|
|
<>
|
|
|
|
|
{labels.required}
|
|
|
|
|
<span className="sr-only">: {name}</span>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
onCheckedChange={(required) =>
|
|
|
|
|
onChange(
|
|
|
|
|
fields.map((item) =>
|
|
|
|
|
item.fieldId === field.fieldId ? { ...item, required } : item
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
{sections.length > 0 && (
|
|
|
|
|
<div className="min-w-40 flex-1">
|
|
|
|
|
<SelectWithAction
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
id={`${prefix}-${field.fieldId}-section`}
|
|
|
|
|
label={`${labels.assign}: ${name}`}
|
|
|
|
|
onValueChange={(section) =>
|
|
|
|
|
onChange(
|
|
|
|
|
fields.map((item) =>
|
|
|
|
|
item.fieldId === field.fieldId
|
|
|
|
|
? {
|
|
|
|
|
fieldId: item.fieldId,
|
|
|
|
|
required: item.required,
|
|
|
|
|
...(section ? { sectionId: section } : {}),
|
|
|
|
|
}
|
|
|
|
|
: item
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
options={[
|
|
|
|
|
...sections.map((section) => ({
|
|
|
|
|
value: section.id,
|
|
|
|
|
label: section.name || labels.name,
|
|
|
|
|
})),
|
|
|
|
|
]}
|
|
|
|
|
placeholder={labels.ungrouped}
|
|
|
|
|
value={field.sectionId ?? ""}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|