173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import { Button } from "@repo/shadcn-ui/components/button";
|
|
import { useId } from "react";
|
|
|
|
import { useDragReorder } from "../hooks/use-drag-reorder";
|
|
import { PlusIcon } from "../lib/icons";
|
|
import { removeTemplateSection } from "../lib/template-sections";
|
|
|
|
import type { FieldSection } from "./field-sections";
|
|
import { FormSectionTitle } from "./form-section-title";
|
|
import { ReorderDropIndicator } from "./reorder-drop-indicator";
|
|
import { ReorderHandle } from "./reorder-handle";
|
|
import { TemplateSectionEditor } from "./template-section-editor";
|
|
import { TemplateSectionFields } from "./template-section-fields";
|
|
|
|
export type SectionBinding = { fieldId: string; required: boolean; sectionId?: string | undefined };
|
|
|
|
export type SectionEditorLabels = {
|
|
title: string;
|
|
expand: string;
|
|
collapse: string;
|
|
fields: string;
|
|
reorder: string;
|
|
reorderHelp: 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,
|
|
addFieldAction,
|
|
fieldLayout = "cards",
|
|
sectionLayout = "stack",
|
|
}: {
|
|
fieldLayout?: "cards" | "compact";
|
|
sectionLayout?: "stack" | "responsive";
|
|
sections: readonly FieldSection[];
|
|
fields: readonly SectionBinding[];
|
|
fieldNames: Readonly<Record<string, string>>;
|
|
fieldNotes?: Readonly<Record<string, string>> | undefined;
|
|
labels: SectionEditorLabels;
|
|
disabled?: boolean;
|
|
createId: () => string;
|
|
addFieldAction?: { label: string; onClick: (sectionId: string | undefined) => void };
|
|
onChange: (value: { sections: FieldSection[]; fields: SectionBinding[] }) => void;
|
|
}) {
|
|
const prefix = useId();
|
|
|
|
const saveSections = (next: FieldSection[]) => onChange({ sections: next, fields: [...fields] });
|
|
|
|
const { getHandleProps, getItemProps, getDropPlacement } = useDragReorder({
|
|
items: sections,
|
|
getId: (section) => section.id,
|
|
disabled,
|
|
onChange: saveSections,
|
|
});
|
|
|
|
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
|
|
addAction={
|
|
addFieldAction
|
|
? {
|
|
label: addFieldAction.label,
|
|
onClick: () => addFieldAction.onClick(sectionId),
|
|
}
|
|
: undefined
|
|
}
|
|
disabled={disabled}
|
|
fieldNames={fieldNames}
|
|
fieldNotes={fieldNotes}
|
|
fields={fields}
|
|
labels={labels}
|
|
layout={fieldLayout}
|
|
onChange={(next) => onChange({ sections: [...sections], fields: next })}
|
|
sectionId={sectionId}
|
|
sections={sections}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<section aria-labelledby={`${prefix}-title`} className="@container/template-sections space-y-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<FormSectionTitle id={`${prefix}-title`}>{labels.title}</FormSectionTitle>
|
|
<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="sr-only" id={`${prefix}-reorder-help`}>
|
|
{labels.reorderHelp}
|
|
</p>
|
|
<div
|
|
className={
|
|
sectionLayout === "responsive"
|
|
? "grid grid-cols-1 items-stretch gap-4 @min-[1000px]/template-sections:grid-cols-2"
|
|
: "space-y-5"
|
|
}
|
|
>
|
|
{sections.map((section, index) => (
|
|
<fieldset
|
|
{...getItemProps(section.id)}
|
|
className="relative min-w-0 overflow-hidden rounded-lg border border-border bg-muted/30 data-[dragging=true]:opacity-50"
|
|
disabled={disabled}
|
|
key={section.id}
|
|
>
|
|
<legend className="sr-only">{section.name || labels.name}</legend>
|
|
<ReorderDropIndicator placement={getDropPlacement(section.id)} />
|
|
<TemplateSectionEditor
|
|
disabled={disabled}
|
|
dragHandle={
|
|
<ReorderHandle
|
|
{...getHandleProps(section.id)}
|
|
aria-describedby={`${prefix}-reorder-help`}
|
|
label={`${labels.reorder}: ${section.name || labels.name} (${index + 1}/${sections.length})`}
|
|
/>
|
|
}
|
|
index={index}
|
|
labels={labels}
|
|
onChange={(next) =>
|
|
saveSections(sections.map((item) => (item.id === section.id ? next : item)))
|
|
}
|
|
onMove={(offset) => moveSection(index, offset)}
|
|
onRemove={() => removeSection(section.id)}
|
|
section={section}
|
|
sectionCount={sections.length}
|
|
>
|
|
{fieldList(section.id)}
|
|
</TemplateSectionEditor>
|
|
</fieldset>
|
|
))}
|
|
</div>
|
|
{fieldList()}
|
|
</section>
|
|
);
|
|
}
|