// * Packages import { Dispatch, SetStateAction, useState } from "react"; import { DndContext, closestCenter, useSensor, useSensors, PointerSensor, DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { TrashIcon, GripVerticalIcon, CheckIcon, CopyIcon } from "lucide-react"; import { usePage } from "@inertiajs/react"; // * Components shadcn import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Card, CardContent } from "@/components/ui/card"; // * Other import EnumSelect from "@/back/Components/Inputs/EnumSelect"; import SimpleString from "@/back/Components/Inputs/SimpleString"; import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import AddNewFieldBtn from "@/back/Components/PageConfigs/AddNewFieldBtn"; import { ConfigField, ConfigInternalField, DynamicFieldTypes, } from "@/back/types/dynamic-pages"; import useTrans from "@/hooks/use-translations"; import str from "@/hooks/use-string"; import { cn } from "@/lib/utils"; /* eslint-disable no-unused-vars */ type FieldGroupProps = { currentId: number; currentIndex: number; currentFieldKey: string; parentFieldKey: string; currentField: ConfigInternalField; currentRequired: boolean; currentDisabled: boolean; getTransInput: ( field: "type" | "key" | "label" | "validation" | "children", id: number, ) => string; updateField: (id: number, updatedField: Partial) => void; removeField: (id: number) => void; transDelete: string; transDragAndDrop?: string; sensitiveField: string[]; removeSensitiveField: (key: string) => void; localErrors: Record; setLocalErrors: Dispatch>>; parentAccordionState: string; setParentAccordionState: (key: string) => void; limitMaxChildren: number; }; /* eslint-enable no-unused-vars */ export default function FieldGroup({ currentId, currentIndex, currentFieldKey, parentFieldKey, currentField, currentRequired, currentDisabled, getTransInput, updateField, removeField, transDelete, transDragAndDrop, sensitiveField, removeSensitiveField, localErrors, setLocalErrors, parentAccordionState, setParentAccordionState, limitMaxChildren, }: FieldGroupProps) { // Drag & drop module for user interactions (mouse, touch, keyboard or other). const sensors = useSensors(useSensor(PointerSensor)); // Provides the necessary props and helpers to make this item draggable and reorderable. // Includes event listeners, accessibility attributes, DOM ref, drag transform/transition values, // and a flag indicating whether the item is dragging. const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: String(currentId) }); // Style applied to the sortable item: // - handles drag movement (transform), // - animations (transition), // - and visual feedback (opacity, zIndex) while dragging. const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, zIndex: isDragging ? 50 : 0, }; // Get all enum values, labels and names. const enumDynamicFieldTypes = usePage().props .enumDynamicFieldTypes as DynamicFieldTypes; // Set the current state of the accordion. const [openIndex, setOpenIndex] = useState(""); // Get the current enum value. const currentEnum = Object.values(enumDynamicFieldTypes).find( (def) => def.value === currentField.type, ); // Set a prefix related to the current enum. const enumCurrentBase = `${currentEnum?.name}_`; // Set the editable part of the current key. const [editableKey, setEditableKey] = useState(() => { return currentField.key.startsWith(enumCurrentBase) ? currentField.key.replace(enumCurrentBase, "") : currentField.key; }); //* Translations const transKeyAlreadyPresent = useTrans("validation.key_already_present"); // Set the complete key (prefix + editable) that will be sent to the backend. const completeKey = `${enumCurrentBase}${editableKey}`; // Set the state of copying key. const [copied, setCopied] = useState(false); // Copy the complete key in the clipboard. const handleClick = () => { navigator.clipboard.writeText(completeKey); setCopied(true); setTimeout(() => setCopied(false), 2000); }; /** * Reorder the elements list. * @param event DragEndEvent * @return void */ const handleOnDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = currentField.children?.findIndex( (c) => String(c.id) === String(active.id), ) ?? -1; const newIndex = currentField.children?.findIndex( (c) => String(c.id) === String(over.id), ) ?? -1; if (oldIndex < 0 || newIndex < 0) return; const reordered = arrayMove( currentField.children!, oldIndex, newIndex, ).map((child, index) => ({ ...child, order: index + 1, })); updateField(currentId, { children: reordered }); }; /** * Add a new field to the list. * @return void */ const handleAddNewChild = () => { const existing = currentField.children ?? []; const maxOrder = Math.max(0, ...existing.map((field) => field.order ?? 0)) + 1; const maxIndex = Math.max(0, ...existing.map((field) => field.id ?? 0)) + 1; const newChild: ConfigInternalField = { id: maxIndex, key: `${enumDynamicFieldTypes.string.name}_`, label: "", type: enumDynamicFieldTypes.string.value, validation: enumDynamicFieldTypes.string.validation, order: maxOrder, children: [], }; const newChildren = [...(currentField.children ?? []), newChild]; updateField(currentId, { children: newChildren }); }; /** * Remove a specific element from the list. * @param childId number * @return void */ const handleDeleteChild = (childId: number) => { const children = currentField.children ?? []; const targetChild = children.filter((child) => child.id === childId)[0]; removeSensitiveField(`${parentFieldKey}.${targetChild.key}`); const childrenUpdated = children.filter((c) => c.id !== childId); updateField(currentId, { children: childrenUpdated }); }; /** * Update the input type. * @param value string * @return void */ const updatingType = (value: string) => { const selectedType = Object.values(enumDynamicFieldTypes).find( (def) => def.value === Number(value), ); if (selectedType) { const resetCommon = { type: selectedType.value, validation: selectedType.validation, key: `${selectedType.name}_${editableKey}`, children: undefined, }; updateField(currentId, resetCommon); } }; return (
key.startsWith( `${currentFieldKey}.${currentIndex}`, ), ), })} > {/* HEADER */}
} tooltipContent={transDragAndDrop} className="cursor-grab touch-none h-full w-full" type="button" variant="outline" size="icon" disabled={parentAccordionState !== ""} /* eslint-disable react/jsx-props-no-spreading */ {...attributes} {...listeners} /* eslint-enable react/jsx-props-no-spreading */ />
} tooltipContent={

{str( useTrans( "back.config_complete_label", ), ) .ucFirst() .value()}

{completeKey}

} type="button" variant="ghost" size="icon" onClick={handleClick} /> } tooltipContent={transDelete} type="button" variant="destructive" size="icon" onClick={() => { if ( parentAccordionState === `item-${currentId}` ) { setParentAccordionState(""); } removeField(currentId); }} />
{currentField.label.length ? currentField.label : `Champs n°${currentIndex + 1}`}
{/* CONTENT */}
updateField(currentId, { label: value }) } parentErrors={localErrors} /> {!sensitiveField.includes( `${parentFieldKey}.${completeKey}`, ) && ( <> { const newCompleteKey = `${enumCurrentBase}${value}`; if ( sensitiveField.includes( `${parentFieldKey}.${newCompleteKey}`, ) ) { setLocalErrors((prev) => ({ ...prev, [`${currentFieldKey}.${currentIndex}.key`]: transKeyAlreadyPresent, })); } else { setLocalErrors((prev) => { const { [`${currentFieldKey}.${currentIndex}.key`]: // eslint-disable-next-line @typescript-eslint/no-unused-vars removedKey, ...rest } = prev; return rest; }); setEditableKey(value); updateField(currentId, { key: newCompleteKey, }); } }} pattern="^[a-z0-9]+(?:[\-_][a-z0-9]+)*$" parentErrors={localErrors} /> { updatingType(value); }} required={currentRequired} disabled={currentDisabled} parentErrors={localErrors} /> )} updateField(currentId, { validation: value, }) } required={currentRequired} disabled={currentDisabled} parentErrors={localErrors} />
{/* CHILDREN */} {currentEnum?.isarray && ( String(c.id))} strategy={ verticalListSortingStrategy } > {(currentField.children ?? []).map( ( child: ConfigInternalField, i: number, ) => ( , ) => { const newChildren = ( currentField.children ?? [] ).map((c) => c.id === childId ? { ...c, ...updatedChild, } : c, ); updateField( currentId, { children: newChildren, }, ); }} removeField={ handleDeleteChild } transDelete={ transDelete } transDragAndDrop={ transDragAndDrop } sensitiveField={ sensitiveField } removeSensitiveField={ removeSensitiveField } localErrors={ localErrors } setLocalErrors={ setLocalErrors } parentAccordionState={ openIndex } setParentAccordionState={ setOpenIndex } limitMaxChildren={ limitMaxChildren } /> ), )} )}
); }