// * Packages import { CSS } from "@dnd-kit/utilities"; import { GripVerticalIcon, TrashIcon } from "lucide-react"; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { useState } from "react"; import { usePage } from "@inertiajs/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/global"; import { cn } from "@/lib/utils"; 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; } // Allows to get recursive list inputs. export default function DynamicList({ name, parentName, value, enumLabel, field, content, isChildren, setData, reset, }: DynamicListProps) { // Notice user there must be children config. if (!field.children && field.children!.length > 0) { return (

{useTrans('back.list_children_needed', { params: { enumLabel } })}

); } // 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(""); /** * Get all elements without each unique identification values. * @param fields DynamicInternalField[] * @returns Omit[] */ const removeIds = (fields: DynamicInternalField[]): Omit[] => { return fields.map(({ id, ...rest }) => { const newObj: any = {}; for (const key in rest) { const value = rest[key as keyof typeof rest]; // If it is an array, we treat it recursively. if (Array.isArray(value)) { newObj[key] = removeIds(value); } // Otherwise, we keep the value as it is. else { newObj[key] = value; } } return newObj; }); }; /** * Remove a specific element from the list. * @param parentName string * @param fieldKey string * @param index number * @return void */ const handleRemoveField = (parentName: string, fieldKey: string, index: number) => { // Get the current list of elements. const list = Array.isArray(content?.[fieldKey as keyof typeof content]) // @ts-ignore ? [...content[fieldKey as keyof typeof content]] : []; // Remove the targeted element. list.splice(index, 1); // Save the new list updated. setData(`${parentName}.${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((field) => field.order ?? 0)) + 1; const maxIndex = Math.max(0, ...sortedListItems.map((field) => field.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) => ( ))} )}
) } type SortableItemProps = { id: number; item: any; itemIndex: number; name: string; parentName: string; field: any; 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; } // 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 !== ""} {...listeners} {...attributes} />
{/* 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. */}
); }