// * Packages import { CSS } from "@dnd-kit/utilities"; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { usePage } from "@inertiajs/react"; import { useState } from "react"; import { GripVerticalIcon, TrashIcon } from "lucide-react"; // * Components shadcn import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Card, CardContent } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; // * Other import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import AddNewFieldBtn from "@/back/Components/PageConfigs/AddNewFieldBtn"; import DynamicInputs from "@/back/Components/DynamicPages/DynamicInputs"; import useTrans from "@/hooks/use-translations"; import { extractMinMaxFromValidation, extractRequiredValidation, } from "@/hooks/use-laravel-validation"; import { ConfigField, DynamicInternalField } from "@/back/types/dynamic-pages"; import { cn, GetTranslation } from "@/lib/utils"; /* eslint-disable no-unused-vars */ type SortableItemProps = { id: number; item: DynamicInternalField | null; itemIndex: number; name: string; parentName: string; field: ConfigField; setData: ( name: string, value: string | boolean | File | object | null, ) => void; reset: (name?: string) => void; removeField: (parentName: string, fieldKey: string, index: number) => void; parentAccordionState: string; setParentAccordionState: (key: string) => void; localErrors: Record; }; /* eslint-enable no-unused-vars */ // Recursive component. function SortableItem({ id, item, itemIndex, name, parentName, field, setData, reset, removeField, parentAccordionState, setParentAccordionState, localErrors, }: SortableItemProps) { // 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 }); // 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, }; // Translations. const transDragAndDrop = useTrans("back.dnd_field"); const transDelete = useTrans("back.delete_field"); return (
key.startsWith(`${name}.${itemIndex}`), ), })} >
{/* Define a drag-and-drop button to swap the order between the current element and another one. */}
} tooltipContent={transDragAndDrop} className="cursor-grab h-full w-full" type="button" variant="outline" size="icon" disabled={parentAccordionState !== ""} /* eslint-disable react/jsx-props-no-spreading */ {...listeners} {...attributes} /* eslint-enable react/jsx-props-no-spreading */ />
{/* Define a delete button to destroy the current element. */} } tooltipContent={transDelete} type="button" variant="destructive" size="icon" onClick={() => { if ( parentAccordionState === `item-${itemIndex}` ) { setParentAccordionState(""); } removeField( parentName, field.key, itemIndex, ); }} />
{/* Define the trigger button to open or close the current accordion. */}
Élément n°{itemIndex + 1}
{/* Get specific input from the config value. */} {field.children?.length && ( )}
); } /* eslint-disable no-unused-vars */ type DynamicListProps = { name: string; parentName: string; value: DynamicInternalField[]; enumLabel: string; field: ConfigField; content: DynamicInternalField | null; isChildren: boolean; setData: ( name: string, value: string | boolean | File | object | null, ) => void; reset: (name?: string) => void; }; /* eslint-enable no-unused-vars */ // Allows to get recursive list inputs. export default function DynamicList({ name, parentName, value, enumLabel, field, content, isChildren, setData, reset, }: DynamicListProps) { // Errors. const { errors } = usePage().props; const [localErrors, setLocalErrors] = useState>(errors); // Drag & drop module for user interactions (mouse, touch, keyboard or other). const sensors = useSensors(useSensor(PointerSensor)); // Define a local identifier for each element to enable unique identification during drag and drop. const localValue = value.map((elmt, index) => { return { ...elmt, id: elmt.id ?? index + 1 }; }); // Order all elements depending on order values. const sortedListItems = [...localValue].sort((a, b) => a.order - b.order); // Extract some values from laravel rules validation. const isRequired = extractRequiredValidation(field.validation); const configLimitMax = extractMinMaxFromValidation(field.validation).max ?? 1; // Accordion open index. const [openIndex, setOpenIndex] = useState(""); // Notice user there must be children config. if (!field.children && field.children!.length > 0) { return (

{GetTranslation("back.list_children_needed", { params: { enumLabel }, })}

); } /** * Remove a specific element from the list. * @param currentParentName string * @param fieldKey string * @param index number * @return void */ const handleRemoveField = ( currentParentName: string, fieldKey: string, index: number, ) => { // Get the current list of elements. const list = Array.isArray(content?.[fieldKey as keyof typeof content]) ? // @ts-expect-error Weird error type. [...content[fieldKey as keyof typeof content]] : []; // Remove the targeted element. list.splice(index, 1); // Save the new list updated. setData(`${currentParentName}.${fieldKey}`, list); // Reset all errors because of alter elements list. setLocalErrors({}); }; /** * Reorder the elements list. * @param event DragEndEvent * @return void */ const handleOnDragEnd = (event: DragEndEvent) => { const { active, over } = event; // Check the unique identification value. if (!over || active.id === over.id) return; // Get the two elements that are switchable by drag and drop. const oldIndex = sortedListItems.findIndex((i) => i.id === active.id); const newIndex = sortedListItems.findIndex((i) => i.id === over.id); // Reorder all elements list to update visually all elements. const reorderedList = arrayMove( sortedListItems, oldIndex, newIndex, ).map((item, index) => ({ ...item, order: index + 1, })); // Save the new ordered list elements. setData(`${parentName}.${field.key}`, reorderedList); }; /** * Add a new field to the list. * @return void */ const handleAddNewField = () => { // Set new element with unique identification and order. const newItem = Object.fromEntries( (field.children ?? []).map((child) => [child.key, ""]), ); const maxOrder = Math.max( 0, ...sortedListItems.map( (currentField) => currentField.order ?? 0, ), ) + 1; const maxIndex = Math.max( 0, ...sortedListItems.map((currentField) => currentField.id ?? 0), ) + 1; const newIndex = value.length; const newItemWithOrder = { ...newItem, id: maxIndex, order: maxOrder }; // Save the list with the new element. setData(`${parentName}.${field.key}.${newIndex}`, newItemWithOrder); }; return (
{value.length > 0 && ( {/* The main provider for dnd-kit. It manages the entire drag-and-drop system: sensors, movement, collision detection, and lifecycle callbacks (onDragStart, onDragEnd, etc.). */} {/* Defines a reorderable list area. It registers sortable items and applies the chosen sorting strategy (vertical, horizontal, grid) to handle item repositioning. */} item.id)} strategy={verticalListSortingStrategy} > {/* Use the recursive component for each elements. */} {sortedListItems.map((item, itemIndex) => ( handleRemoveField( currentParentName, fieldKey, index, ) } parentAccordionState={openIndex} setParentAccordionState={setOpenIndex} localErrors={localErrors} /> ))} )}
); }