metis-desk/packages/rdx-ui/src/components/template-section-fields.tsx

160 lines
5.5 KiB
TypeScript
Raw Normal View History

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
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
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
next[index] = neighbor;
next[other] = current;
onChange(next);
};
2026-10-10 10:01:58 +00:00
if (!(sectionId || grouped.length)) {
return null;
}
2026-10-10 10:01:58 +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
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>
);
}