// * Packages import { useState, useEffect, useMemo, useRef } from "react"; import { usePage } from "@inertiajs/react"; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import type { DragEndEvent } from "@dnd-kit/core"; // * Components shadcn import { Accordion } from "@/components/ui/accordion"; // * Other import FieldGroup from "@/back/Components/PageConfigs/FieldGroup"; import AddNewFieldBtn from "@/back/Components/PageConfigs/AddNewFieldBtn"; import InputError from "@/back/Components/InputError"; import { ConfigField, ConfigInternalField, DynamicFieldTypes, } from "@/back/types/dynamic-pages"; import useTrans from "@/hooks/use-translations"; import str from "@/hooks/use-string"; type DynamicFieldsProps = { name: string; value: ConfigField[]; required?: boolean; disabled?: boolean; selectedConfig: string | null; // eslint-disable-next-line no-unused-vars setData: (name: string, value: ConfigField[]) => void; }; export default function DynamicFields({ name, value = [], required = true, disabled = false, selectedConfig = null, setData, }: DynamicFieldsProps) { // Errors. const { errors } = usePage().props; const [localErrors, setLocalErrors] = useState>(errors); // Enums. const enumDynamicFieldTypes = usePage().props .enumDynamicFieldTypes as DynamicFieldTypes; // 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, ): ConfigInternalField[] => { let currentId = startId; const withIds = fields.map((field) => { currentId += 1; const id = currentId; // Add the key in sensitive array. const currentParentKey = `${parentKey}.${field.key}`; setOriginalConfigKeys((prev) => [...prev, currentParentKey]); const children = field.children ? addIdsRecursively(field.children, currentParentKey, currentId) : undefined; if (field.children?.length) { currentId += field.children.length; } return children ? { ...field, id, children } : { ...field, id }; }); return withIds as ConfigInternalField[]; }; const [fields, setFields] = useState(() => addIdsRecursively(value ?? []), ); // Sorted fields by order. const sortedFields = useMemo( () => [...fields].sort((a, b) => (a.order ?? 1) - (b.order ?? 1)), [fields], ); // Drag & drop module for user interactions (mouse, touch, keyboard or other). const sensors = useSensors(useSensor(PointerSensor)); // Translations needed. const trans = { key: useTrans("validation.custom.key"), label: useTrans("validation.custom.label"), type: useTrans("validation.custom.type"), validation: useTrans("validation.custom.validation"), children: useTrans("validation.custom.children"), }; const transJunction = useTrans("validation.custom.of_the.male"); const transField = useTrans("validation.custom.field"); const transDelete = useTrans("back.delete_field"); const transDragAndDrop = useTrans("back.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 trans, id: number): string => { const transFieldValue = trans[field] ?? trans.label; return str(`${transFieldValue} ${transJunction} ${transField} n°${id}`) .ucFirst() .value(); }; /** * Add a new field. * @returns void */ const addField = (): void => { const maxOrder = Math.max(0, ...fields.map((field) => field.order ?? 0)) + 1; const maxIndex = Math.max(0, ...fields.map((field) => field.id ?? 0)) + 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) => { if (field.id !== fieldId) return field; // Merge the data. const merged = { ...field, ...updatedField }; // 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.filter((field) => field.id === fieldId)[0]; 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) } : {}), })) as ConfigField[]; }; // 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]); // Update fields when copying config from another locale. useEffect(() => { setFields(() => addIdsRecursively(value ?? [])); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedConfig]); 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} /> ))} )}
); }