import type { DragEndEvent } from "@dnd-kit/core"; import { closestCenter, DndContext, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { usePage } from "@inertiajs/react"; import { useEffect, useMemo, useRef, useState } from "react"; import InputError from "@/back/Components/InputError"; import AddNewFieldBtn from "@/back/Components/PageConfigs/AddNewFieldBtn"; import FieldGroup from "@/back/Components/PageConfigs/FieldGroup"; import type { ConfigField, ConfigInternalField, DynamicFieldType, } from "@/back/types/dynamic-pages"; import { Accordion } from "@/components/ui/accordion"; import str from "@/hooks/use-string"; import trans from "@/hooks/use-translations"; interface DynamicFieldsProps { name: string; value: ConfigField[]; required?: boolean; disabled?: boolean; selectedConfig: string | null; setData: (name: string, value: ConfigField[]) => void; } export default function DynamicFields({ name, value, required = true, disabled = false, selectedConfig, setData, }: DynamicFieldsProps) { // Errors. const { errors } = usePage().props; const [localErrors, setLocalErrors] = useState>(errors); const [prevErrors, setPrevErrors] = useState>(errors); if (errors !== prevErrors) { setPrevErrors(errors); setLocalErrors(errors); } // Enums. const enumDynamicFieldTypes = usePage().props .enumDynamicFieldTypes as DynamicFieldType; // Accordion open index. const [openIndex, setOpenIndex] = useState(""); // Local changes of fields (avoid loop between useEffects). const prevValueRef = useRef(JSON.stringify(value)); // Sensitive fields. const [originalConfigKeys, setOriginalConfigKeys] = useState([]); const addIdsRecursively = ( fields: ConfigField[], parentKey: string = name, startId = 0, collectedKeys: string[] = [], ): ConfigInternalField[] => { let currentId = startId; const withIds = fields.map((field) => { currentId += 1; const id = currentId; const currentParentKey = `${parentKey}.${field.key}`; collectedKeys.push(currentParentKey); const children = field.children ? addIdsRecursively( field.children, currentParentKey, currentId, collectedKeys, ) : undefined; if (field.children?.length) { currentId += field.children.length; } return children ? { ...field, id, children } : { ...field, id }; }); return withIds as ConfigInternalField[]; }; const [fields, setFields] = useState(() => { const keys: string[] = []; const result = addIdsRecursively(value, name, 0, keys); setOriginalConfigKeys(keys); return result; }); const [prevSelectedConfig, setPrevSelectedConfig] = useState( selectedConfig, ); if (selectedConfig !== prevSelectedConfig) { setPrevSelectedConfig(selectedConfig); const keys: string[] = []; const result = addIdsRecursively(value, name, 0, keys); setFields(result); setOriginalConfigKeys(keys); } // Sorted fields by order. const sortedFields = useMemo( () => [...fields].sort((a, b) => a.order - b.order), [fields], ); // Drag & drop module for user interactions (mouse, touch, keyboard or other). const sensors = useSensors(useSensor(PointerSensor)); // Translations needed. const transFields = { key: trans("uwil-dynamic-pages::trans.attributes.key"), label: trans("uwil-dynamic-pages::trans.attributes.label"), type: trans("uwil-dynamic-pages::trans.attributes.type"), validation: trans("uwil-dynamic-pages::trans.attributes.validation"), children: trans("uwil-dynamic-pages::trans.attributes.children"), }; const transJunction = trans( "uwil-dynamic-pages::trans.attributes.of_the.male", ); const transField = trans("uwil-dynamic-pages::trans.attributes.field"); const transDelete = trans("uwil-dynamic-pages::trans.delete_field"); const transDragAndDrop = trans("uwil-dynamic-pages::trans.dnd_field"); // Set the depth limit of each list elements. const MAX_DEPTH = 10; /** * Get a translation for a specific input from trans constant. * @param field string * @param id number * @returns string */ const getTransInput = ( field: keyof typeof transFields, id: number, ): string => { return str( `${transFields[field]} ${transJunction} ${transField} n°${String(id)}`, ) .ucFirst() .value(); }; /** * Add a new field. * @returns void */ const addField = (): void => { const maxOrder = Math.max(0, ...fields.map((field) => field.order)) + 1; const maxIndex = Math.max(0, ...fields.map((field) => field.id)) + 1; const newField: ConfigInternalField = { id: maxIndex, key: `${enumDynamicFieldTypes.string.name}_`, type: enumDynamicFieldTypes.string.value, label: "", validation: enumDynamicFieldTypes.string.validation, order: maxOrder, }; setFields((prev) => [...prev, newField]); }; /** * Update a specific field. * @param fieldId string * @param updatedField Partial * @returns void */ const updateField = ( fieldId: number, updatedField: Partial, ): void => { setFields((prev) => prev.map((field): ConfigInternalField => { if (field.id !== fieldId) return field; // Merge the data. const merged: ConfigInternalField = { ...field, ...updatedField, // Préserve les children typés ConfigInternalField si updatedField n'en fournit pas children: (updatedField.children as ConfigInternalField[] | undefined) ?? field.children, }; // Only if updated field is type. if (updatedField.type !== undefined) { const matchingKey = Object.keys(enumDynamicFieldTypes).find( (k) => enumDynamicFieldTypes[k].value === updatedField.type, ); const originalMatchingKey = Object.keys(enumDynamicFieldTypes).find( (k) => enumDynamicFieldTypes[k].value === field.type, ); const editableKey = field.key.split( `${originalMatchingKey ?? enumDynamicFieldTypes.string.name}_`, )[1]; if (matchingKey) { merged.validation = enumDynamicFieldTypes[matchingKey].validation; merged.type = enumDynamicFieldTypes[matchingKey].value; merged.key = `${matchingKey}_${editableKey}`; } } return merged; }), ); }; /** * Remove sensitive field list. * @param keyToRemove string */ const removeSensitiveField = (keyToRemove: string): void => { setOriginalConfigKeys((prev) => prev.filter((key) => key !== keyToRemove)); // Reset all errors because of alter elements list. setLocalErrors({}); }; /** * Remove a specific field. * @param fieldId string * @returns void */ const removeField = (fieldId: number): void => { const targetField = fields.find((field) => field.id === fieldId); if (!targetField) return; removeSensitiveField(targetField.key); setFields((prev) => prev.filter((field) => field.id !== fieldId)); }; /** * Save the new order. * @param event DragEndEvent * @return void */ const handleDragEnd = (event: DragEndEvent): void => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = sortedFields.findIndex((f) => f.id === Number(active.id)); const newIndex = sortedFields.findIndex((f) => f.id === Number(over.id)); if (oldIndex < 0 || newIndex < 0) return; setFields((prev) => { const moved = arrayMove(prev, oldIndex, newIndex); return moved.map((fld, idx) => ({ ...fld, order: idx + 1, })); }); }; /** * Remove all local ids. * @param currentFields ConfigInternalField[] * @returns */ const removeIds = (currentFields: ConfigInternalField[]): ConfigField[] => { // Delete the id, keep only the children if they are present, and keep the rest. // eslint-disable-next-line @typescript-eslint/no-unused-vars return currentFields.map(({ id, children, ...rest }) => ({ ...rest, ...(children ? { children: removeIds(children) } : {}), })); }; // Export to parent only if payload differs with prevValueRef. useEffect(() => { const payload = removeIds(fields); const payloadStr = JSON.stringify(payload); if (payloadStr !== prevValueRef.current) { setData(name, payload); prevValueRef.current = payloadStr; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [fields]); return (
{sortedFields.length > 0 && ( String(field.id))} strategy={verticalListSortingStrategy} > {sortedFields.map((field, index) => ( { removeField(field.id); }} updateField={(_, updatedInput) => { updateField(field.id, updatedInput); }} currentRequired={required} currentDisabled={disabled} sensitiveField={originalConfigKeys} getTransInput={getTransInput} transDelete={transDelete} transDragAndDrop={transDragAndDrop} removeSensitiveField={removeSensitiveField} localErrors={localErrors} setLocalErrors={setLocalErrors} parentAccordionState={openIndex} setParentAccordionState={setOpenIndex} limitMaxChildren={MAX_DEPTH} /> ))} )}
); }