From c6b2e8482a7de57eddbcc09fc528e8f583e911d1 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 17 Nov 2025 10:28:43 +0100 Subject: [PATCH] fix: :art: clean & comment dynamic pages components --- lang/en/back.php | 1 + lang/fr/back.php | 1 + .../Components/DynamicPages/DynamicInputs.tsx | 9 +- .../Components/DynamicPages/DynamicList.tsx | 148 +++++++++++++----- .../DynamicPages/DynamicStatusBadge.tsx | 13 +- resources/ts/back/Pages/DynamicPages/Form.tsx | 10 +- 6 files changed, 132 insertions(+), 50 deletions(-) diff --git a/lang/en/back.php b/lang/en/back.php index 9a421473..254f206b 100644 --- a/lang/en/back.php +++ b/lang/en/back.php @@ -100,6 +100,7 @@ return [ "new_field" => "Add a new field", "delete_field" => "Delete this field", "dnd_field" => "Move this field", + "list_children_needed" => "Children are required for this type :enumLabel", "empty_warning" => "No data recorded", "page_locked" => "This page is currently locked", "unsupported_type" => "Unsupported type:", diff --git a/lang/fr/back.php b/lang/fr/back.php index f2f0a06a..4593de19 100644 --- a/lang/fr/back.php +++ b/lang/fr/back.php @@ -100,6 +100,7 @@ return [ "new_field" => "Ajouter un nouveau champ", "delete_field" => "Supprimer ce champ", "dnd_field" => "Déplacer ce champ", + "list_children_needed" => "Des enfants sont nécessaires pour ce type :enumLabel", "empty_warning" => "Aucune donnée enregistrée", "page_locked" => "Cette page est actuellement vérouillée", "unsupported_type" => "Type non supporté:", diff --git a/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx b/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx index f5946c74..2b74c28f 100644 --- a/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx +++ b/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx @@ -24,6 +24,7 @@ type DynamicInputsProps = { reset: (name?: string) => void; }; +// Allows to get specific input from enum value. export default function DynamicInputs({ name, config, @@ -31,6 +32,7 @@ export default function DynamicInputs({ setData, reset, }: DynamicInputsProps) { + // Enums. const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes; // Translations. @@ -49,12 +51,17 @@ export default function DynamicInputs({ return (
+ {/* Loop on all config elements. */} {Object.entries(config).map(([id, field]) => { + // Get the enum value from the element type value. const typeEnum = Object.values(enumDynamicFieldTypes).find( (def) => def.value === field.type, ); + // Set the name of the current field. const fieldName = `${name}.${field.key}`; + + // Set the content value from the config key. const fieldValue = content?.[field.key as keyof typeof content] ?? ""; switch (field.type) { @@ -137,7 +144,7 @@ export default function DynamicInputs({ label={getTrans(field.label)} setData={setData} reset={reset} - value={String(fieldValue)} + value={fieldValue as string | File | null} required={extractRequiredValidation(field.validation)} maxFileSize={extractLaravelValidationValue(field.validation, 'max')} allowedFormats={extractLaravelValidationValue(field.validation, 'mimes')} diff --git a/resources/ts/back/Components/DynamicPages/DynamicList.tsx b/resources/ts/back/Components/DynamicPages/DynamicList.tsx index 32ba7902..f6d795de 100644 --- a/resources/ts/back/Components/DynamicPages/DynamicList.tsx +++ b/resources/ts/back/Components/DynamicPages/DynamicList.tsx @@ -1,7 +1,7 @@ // * Packages import { CSS } from "@dnd-kit/utilities"; import { GripVerticalIcon, TrashIcon } from "lucide-react"; -import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; +import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; // * Components shadcn import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; @@ -26,6 +26,7 @@ type DynamicListProps = { reset: (name?: string) => void; } +// Allows to get recursive list inputs. export default function DynamicList({ name, parentName, @@ -40,34 +41,44 @@ export default function DynamicList({ if (!field.children && field.children!.length > 0) { return (

- Des enfants sont nécessaires pour ce type {enumLabel} + {useTrans('back.list_children_needed', { + params: { enumLabel } + })}

); } - // Drag&Drop. + // Drag & drop module for user interactions (mouse, touch, keyboard or other). const sensors = useSensors(useSensor(PointerSensor)); - // Set local id. + // 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; + /** + * 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]; - // Si c’est un tableau, on le traite récursivement + // If it is an array, we treat it recursively. if (Array.isArray(value)) { newObj[key] = removeIds(value); } - // Sinon, on garde la valeur telle quelle + // Otherwise, we keep the value as it is. else { newObj[key] = value; } @@ -76,6 +87,69 @@ export default function DynamicList({ }); }; + /** + * 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); + } + + /** + * 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 (
@@ -85,29 +159,24 @@ export default function DynamicList({ collapsible className="w-full" > + {/* + The main provider for dnd-kit. + It manages the entire drag-and-drop system: sensors, movement, collision detection, and lifecycle callbacks (onDragStart, onDragEnd, etc.). + */} { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const oldIndex = sortedListItems.findIndex((i) => i.id === active.id); - const newIndex = sortedListItems.findIndex((i) => i.id === over.id); - - const reorderedList = arrayMove(sortedListItems, oldIndex, newIndex) - .map((item, index) => ({ - ...item, - order: index + 1, - })); - - setData(`${parentName}.${field.key}`, reorderedList); - }} + onDragEnd={handleOnDragEnd} > + {/* + 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) => ( { - const list = Array.isArray(content?.[fieldKey as keyof typeof content]) - // @ts-ignore - ? [...content[fieldKey as keyof typeof content]] - : []; - list.splice(index, 1); - setData(`${parentName}.${fieldKey}`, list); - }} + removeField={handleRemoveField} /> ))} )} - { - 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 }; - setData(`${parentName}.${field.key}.${newIndex}`, newItemWithOrder); - }} /> +
) } @@ -159,6 +216,7 @@ type SortableItemProps = { removeField: (parentName: string, fieldKey: string, index: number) => void; } +// Recursive component. function SortableItem({ id, item, @@ -170,8 +228,15 @@ function SortableItem({ reset, removeField, }: 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, @@ -190,8 +255,8 @@ function SortableItem({
+ {/* Define a drag-and-drop button to swap the order between the current element and another one. */}
- {/* Drag handle */} } tooltipContent={transDragAndDrop} @@ -203,6 +268,7 @@ function SortableItem({ {...attributes} />
+ {/* Define a delete button to destroy the current element. */} } tooltipContent={transDelete} @@ -212,6 +278,7 @@ function SortableItem({ onClick={() => removeField(parentName, field.key, itemIndex)} />
+ {/* Define the trigger button to open or close the current accordion. */}
Élément n°{itemIndex + 1} @@ -219,6 +286,7 @@ function SortableItem({
+ {/* Get specific input from the config value. */} status.value === value, ); + + // Get the color and icon for the current enum value. const { color, Icon } = statusStyles[ enumValue?.value as keyof typeof statusStyles - ] ?? { - color: "bg-green-200 dark:bg-green-900", - Icon: CheckIcon, - }; + ]; return ( diff --git a/resources/ts/back/Pages/DynamicPages/Form.tsx b/resources/ts/back/Pages/DynamicPages/Form.tsx index a37a332c..f0b587bc 100644 --- a/resources/ts/back/Pages/DynamicPages/Form.tsx +++ b/resources/ts/back/Pages/DynamicPages/Form.tsx @@ -15,6 +15,11 @@ import { DialogFooter, DialogTrigger, } from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; // * Other import SimpleString from "@/back/Components/Inputs/SimpleString"; import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch"; @@ -31,11 +36,6 @@ import str from "@/hooks/use-string"; import { useToast } from "@/hooks/use-toast"; import { Methods } from "@/lib/enums"; import { AppEnv, AuthProp, ConfigField, DynamicPageModel, LaravelEnum, PageConfigModel } from "@/back/types/global"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; export default function Form({ model,