metis-desk/packages/rdx-ui/src/components/template-sections-editor.tsx

170 lines
5.6 KiB
TypeScript
Raw Normal View History

import { Button } from "@repo/shadcn-ui/components/button";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
import { ArrowDownIcon, ArrowUpIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useId } from "react";
import { removeTemplateSection } from "../lib/template-sections";
import type { FieldSection } from "./field-sections";
import { TemplateSectionFields } from "./template-section-fields";
export type SectionBinding = { fieldId: string; required: boolean; sectionId?: string | undefined };
export type SectionEditorLabels = {
title: string;
add: string;
name: string;
description: string;
ungrouped: string;
moveUp: string;
moveDown: string;
remove: string;
removeField: string;
required: string;
assign: string;
help: string;
empty: string;
};
export function TemplateSectionsEditor({
sections,
fields,
fieldNames,
fieldNotes,
labels,
disabled = false,
createId,
onChange,
}: {
sections: readonly FieldSection[];
fields: readonly SectionBinding[];
fieldNames: Readonly<Record<string, string>>;
fieldNotes?: Readonly<Record<string, string>> | undefined;
labels: SectionEditorLabels;
disabled?: boolean;
createId: () => string;
onChange: (value: { sections: FieldSection[]; fields: SectionBinding[] }) => void;
}) {
const prefix = useId();
const saveSections = (next: FieldSection[]) => onChange({ sections: next, fields: [...fields] });
const moveSection = (index: number, offset: number) => {
const next = [...sections];
const first = next[index];
const second = next[index + offset];
if (first && second) {
next[index] = second;
next[index + offset] = first;
saveSections(next);
}
};
const removeSection = (id: string) => onChange(removeTemplateSection(sections, fields, id));
const fieldList = (sectionId?: string | undefined) => (
<TemplateSectionFields
disabled={disabled}
fieldNames={fieldNames}
fieldNotes={fieldNotes}
fields={fields}
labels={labels}
onChange={(next) => onChange({ sections: [...sections], fields: next })}
sectionId={sectionId}
sections={sections}
/>
);
return (
<section className="space-y-3">
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-medium">{labels.title}</h3>
<Button
disabled={disabled || sections.length >= 30}
onClick={() => saveSections([...sections, { id: createId(), name: "", description: "" }])}
size="sm"
type="button"
variant="outline"
>
<PlusIcon aria-hidden="true" />
{labels.add}
</Button>
</div>
<p className="text-xs text-muted-foreground">{labels.help}</p>
2026-10-09 18:30:41 +00:00
<div className="grid items-start gap-4 @min-[900px]:grid-cols-2">
{sections.map((section, index) => (
<fieldset
className="space-y-3 rounded-md border p-3"
disabled={disabled}
key={section.id}
>
<legend className="sr-only">{section.name || labels.name}</legend>
<div className="flex items-end gap-2">
<div className="min-w-0 flex-1 space-y-2">
<Label htmlFor={`${prefix}-${section.id}-name`}>{labels.name}</Label>
<Input
id={`${prefix}-${section.id}-name`}
maxLength={100}
onChange={(event) =>
saveSections(
sections.map((item) =>
item.id === section.id ? { ...item, name: event.target.value } : item
)
)
}
required
value={section.name}
/>
</div>
<Button
aria-label={`${labels.moveUp}: ${section.name}`}
disabled={disabled || index === 0}
onClick={() => moveSection(index, -1)}
size="icon-sm"
type="button"
variant="ghost"
>
<ArrowUpIcon />
</Button>
<Button
aria-label={`${labels.moveDown}: ${section.name}`}
disabled={disabled || index === sections.length - 1}
onClick={() => moveSection(index, 1)}
size="icon-sm"
type="button"
variant="ghost"
>
<ArrowDownIcon />
</Button>
<Button
aria-label={`${labels.remove}: ${section.name}`}
disabled={disabled}
onClick={() => removeSection(section.id)}
size="icon-sm"
type="button"
variant="ghost"
>
<Trash2Icon />
</Button>
</div>
<div className="space-y-2">
<Label htmlFor={`${prefix}-${section.id}-description`}>{labels.description}</Label>
<Textarea
id={`${prefix}-${section.id}-description`}
maxLength={1000}
onChange={(event) =>
saveSections(
sections.map((item) =>
2026-10-09 18:30:41 +00:00
item.id === section.id ? { ...item, description: event.target.value } : item
)
)
}
2026-10-09 18:30:41 +00:00
rows={2}
value={section.description}
/>
</div>
2026-10-09 18:30:41 +00:00
{fieldList(section.id)}
</fieldset>
))}
</div>
{fieldList()}
</section>
);
}