- 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.
131 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|