metis-desk/packages/rdx-ui/src/components/template-section-editor.tsx
David ccb52fb534 feat: add catalog field picker dialog and list components
- Introduced `CatalogFieldPickerDialog` and `CatalogFieldPickerList` for selecting catalog fields.
- Implemented `appendTemplateFields` helper function for managing template fields.
- Added tests for catalog field selection and template field management.
- Created reusable components for field picking and reordering in the UI library.
- Enhanced drag-and-drop functionality for reordering items in lists.
2026-10-15 15:58:01 +02:00

131 lines
4.2 KiB
TypeScript

import { Button } from "@repo/shadcn-ui/components/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@repo/shadcn-ui/components/collapsible";
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, ChevronRightIcon, Trash2Icon } from "lucide-react";
import type { ReactNode } from "react";
import { useId, useState } from "react";
import { flushSync } from "react-dom";
import type { FieldSection } from "./field-sections";
import type { SectionEditorLabels } from "./template-sections-editor";
/** Conserva la edición y el estado de apertura de una sección al reordenarla. */
export function TemplateSectionEditor({
section,
index,
sectionCount,
labels,
disabled,
dragHandle,
onChange,
onMove,
onRemove,
children,
}: {
section: FieldSection;
index: number;
sectionCount: number;
labels: SectionEditorLabels;
disabled: boolean;
dragHandle: ReactNode;
onChange: (section: FieldSection) => void;
onMove: (offset: -1 | 1) => void;
onRemove: () => void;
children: ReactNode;
}) {
const prefix = useId();
const [open, setOpen] = useState(true);
return (
<Collapsible disabled={disabled} onOpenChange={setOpen} open={open}>
<div className="flex flex-wrap items-center gap-3 border-b bg-muted/70 px-4 py-3">
<CollapsibleTrigger
aria-label={`${open ? labels.collapse : labels.expand}: ${section.name || labels.name}`}
render={<Button size="icon-sm" type="button" variant="ghost" />}
>
<ChevronRightIcon aria-hidden="true" className={open ? "rotate-90" : ""} />
</CollapsibleTrigger>
{dragHandle}
<span
aria-hidden="true"
className="flex size-7 shrink-0 items-center justify-center rounded-md border bg-background text-xs font-semibold text-muted-foreground"
>
{index + 1}
</span>
<h4 className="sr-only">{section.name || labels.name}</h4>
<div className="min-w-40 flex-1 space-y-2">
<Input
aria-label={labels.name}
id={`${prefix}-name`}
maxLength={100}
onChange={(event) => onChange({ ...section, name: event.target.value })}
placeholder={labels.name}
required
value={section.name}
/>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
aria-label={`${labels.moveUp}: ${section.name}`}
disabled={disabled || index === 0}
onClick={() => onMove(-1)}
size="icon-sm"
type="button"
variant="ghost"
>
<ArrowUpIcon />
</Button>
<Button
aria-label={`${labels.moveDown}: ${section.name}`}
disabled={disabled || index === sectionCount - 1}
onClick={() => onMove(1)}
size="icon-sm"
type="button"
variant="ghost"
>
<ArrowDownIcon />
</Button>
<Button
aria-label={`${labels.remove}: ${section.name}`}
disabled={disabled}
onClick={onRemove}
size="icon-sm"
type="button"
variant="ghost"
>
<Trash2Icon />
</Button>
</div>
</div>
<CollapsibleContent
className="data-[closed]:hidden"
keepMounted
onInvalidCapture={() => {
// Abrir antes de que el navegador enfoque un control inválido oculto.
flushSync(() => setOpen(true));
}}
>
<div className="space-y-4 p-4">
<div className="space-y-2">
<Label htmlFor={`${prefix}-description`}>{labels.description}</Label>
<Textarea
id={`${prefix}-description`}
maxLength={1000}
onChange={(event) => onChange({ ...section, description: event.target.value })}
rows={2}
value={section.description}
/>
</div>
</div>
<div className="border-t p-4">{children}</div>
</CollapsibleContent>
</Collapsible>
);
}