From a67490f997677224055ebd64130416cc2c9bb378 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 21 Oct 2025 15:35:14 +0200 Subject: [PATCH] fix: :bug: set id locally for each inputs in page configuration --- lang/en/back.php | 1 + lang/fr/back.php | 2 +- .../Components/PageConfigs/DynamicFields.tsx | 80 ++++++++----- .../Components/PageConfigs/SortableField.tsx | 106 ++++++++++++------ 4 files changed, 126 insertions(+), 63 deletions(-) diff --git a/lang/en/back.php b/lang/en/back.php index a711c69d..f323fd5e 100644 --- a/lang/en/back.php +++ b/lang/en/back.php @@ -89,6 +89,7 @@ return [ "config_save_warning" => "The configuration of related pages in :locale will be updated accordingly.", "config_save_warning_list" => "Here is the list of :models concerned:|Here is the list of :models concerned:", "config_archived" => "This configuration is currently archived", + "config_complete_label" => "Copy the complete label of the input:", "unsupported_type" => "Unsupported type:", "missing_translations" => "The translation of the :field for the language :locales is missing.|The translation of the :field for :locales languages are missing.", diff --git a/lang/fr/back.php b/lang/fr/back.php index 01384547..4d0f83ae 100644 --- a/lang/fr/back.php +++ b/lang/fr/back.php @@ -89,7 +89,7 @@ return [ en conséquence.", "config_save_warning_list" => "Voici la liste des :models concernés:|Voici la liste des :models concernées:", "config_archived" => "Cette configuration est actuellement archivée", - "config_complete_label" => "Label complet de l'input:", + "config_complete_label" => "Copier le libellé complet de l'input:", "unsupported_type" => "Type non supporté:", "missing_translations" => "La traduction du champs :field pour la langue :locales est manquante.|La traduction du champs :field pour les langues :locales sont manquantes.", diff --git a/resources/ts/back/Components/PageConfigs/DynamicFields.tsx b/resources/ts/back/Components/PageConfigs/DynamicFields.tsx index 6210206f..e1a75351 100644 --- a/resources/ts/back/Components/PageConfigs/DynamicFields.tsx +++ b/resources/ts/back/Components/PageConfigs/DynamicFields.tsx @@ -24,21 +24,16 @@ import str from "@/hooks/use-string"; import { DynamicField, DynamicFieldTypes } from "@/back/types/global"; import useTrans from "@/hooks/use-translations"; -// Generate unique stable id for each field. -const generateFieldId = (() => { - let count = 0; - return () => `field_id_${++count}`; -})(); - type DynamicFieldsProps = { name: string; value: DynamicField[]; required?: boolean; disabled?: boolean; - setData: (name: string, value: any) => void; + // eslint-disable-next-line no-unused-vars + setData: (name: string, value: DynamicField[]) => void; }; -type InternalField = DynamicField & { id: string }; +type InternalField = DynamicField & { id: number }; export default function DynamicFields({ name, @@ -47,12 +42,20 @@ export default function DynamicFields({ disabled = false, setData, }: DynamicFieldsProps) { - const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes; + const { errors } = usePage().props; + const enumDynamicFieldTypes = usePage().props + .enumDynamicFieldTypes as DynamicFieldTypes; + + // Unique stable id for each field. + const nextId = useRef(1); - // All fields with stable id. const [fields, setFields] = useState(() => - (value ?? []).map((field) => ({ id: generateFieldId(), ...field })) + (value ?? []).map((field) => ({ + id: nextId.current++, + ...field, + })), ); + // Local changes of fields (avoid loop between useEffects). const prevValueRef = useRef(JSON.stringify(value ?? [])); const sensors = useSensors(useSensor(PointerSensor)); @@ -78,13 +81,15 @@ export default function DynamicFields({ */ const getTransInput = (field: keyof typeof trans, id: number): string => { const transFieldValue = trans[field] ?? trans.label; - return str(`${transFieldValue} ${transJunction} ${transField} n°${id}`).ucFirst().value(); + return str(`${transFieldValue} ${transJunction} ${transField} n°${id}`) + .ucFirst() + .value(); }; // Sorted fields by order. const sortedFields = useMemo( () => [...fields].sort((a, b) => (a.order ?? 1) - (b.order ?? 1)), - [fields] + [fields], ); /** @@ -92,17 +97,19 @@ export default function DynamicFields({ * @returns void */ const addField = (): void => { - const label = enumDynamicFieldTypes.string.label; - const maxOrder = Math.max(0, ...fields.map((field) => field.order ?? 0)) + 1; + const { label } = enumDynamicFieldTypes.string; + const maxOrder = + Math.max(0, ...fields.map((field) => field.order ?? 0)) + 1; const newField: InternalField = { - id: generateFieldId(), + id: nextId.current++, key: `field_${maxOrder}`, type: 1, label, validation: enumDynamicFieldTypes.string.validation, order: maxOrder, }; + setFields((prev) => [...prev, newField]); }; @@ -112,19 +119,26 @@ export default function DynamicFields({ * @param updatedField Partial * @returns void */ - const updateField = (fieldId: string, updatedField: Partial): void => { + const updateField = ( + fieldId: number, + updatedField: Partial, + ): void => { setFields((prev) => prev.map((field) => { if (field.id !== fieldId) return field; const merged = { ...field, ...updatedField }; if (updatedField.type !== undefined) { const matchingKey = Object.keys(enumDynamicFieldTypes).find( - (k) => enumDynamicFieldTypes[k].value === updatedField.type + (k) => + enumDynamicFieldTypes[k].value === + updatedField.type, ); - if (matchingKey) merged.validation = enumDynamicFieldTypes[matchingKey].validation; + if (matchingKey) + merged.validation = + enumDynamicFieldTypes[matchingKey].validation; } return merged; - }) + }), ); }; @@ -133,7 +147,7 @@ export default function DynamicFields({ * @param fieldId string * @returns void */ - const removeField = (fieldId: string): void => { + const removeField = (fieldId: number): void => { setFields((prev) => prev.filter((field) => field.id !== fieldId)); }; @@ -146,15 +160,19 @@ export default function DynamicFields({ const { active, over } = event; if (!over || active.id === over.id) return; - const oldIndex = sortedFields.findIndex((f) => f.id === String(active.id)); - const newIndex = sortedFields.findIndex((f) => f.id === String(over.id)); + const oldIndex = sortedFields.findIndex( + (f) => f.id === Number(active.id), + ); + const newIndex = sortedFields.findIndex( + (f) => f.id === Number(over.id), + ); if (oldIndex === -1 || newIndex === -1) return; setFields((prev) => arrayMove(prev, oldIndex, newIndex).map((fld, idx) => ({ ...fld, order: idx + 1, - })) + })), ); }; @@ -173,18 +191,18 @@ export default function DynamicFields({ if (!value) return; const valueStr = JSON.stringify(value ?? []); - // Already synchronized. + // Already synchronized. if (valueStr === prevValueRef.current) return; setFields((prev) => (value ?? []).map((field, i) => { const existing = prev.find((p) => p.key === field.key); return { - id: existing?.id ?? generateFieldId(), + id: existing?.id ?? nextId.current++, ...field, order: field.order ?? i + 1, } as InternalField; - }) + }), ); prevValueRef.current = valueStr; @@ -205,12 +223,14 @@ export default function DynamicFields({ {sortedFields.map((field, index) => ( removeField(field.id)} - updateField={(_, updatedInput) => updateField(field.id, updatedInput)} + updateField={(_, updatedInput) => + updateField(field.id, updatedInput) + } required={required} disabled={disabled} getTransInput={getTransInput} @@ -231,7 +251,7 @@ export default function DynamicFields({ {useTrans("back.config_new_field")} - + ); } diff --git a/resources/ts/back/Components/PageConfigs/SortableField.tsx b/resources/ts/back/Components/PageConfigs/SortableField.tsx index 088e5002..85e0fab6 100644 --- a/resources/ts/back/Components/PageConfigs/SortableField.tsx +++ b/resources/ts/back/Components/PageConfigs/SortableField.tsx @@ -1,5 +1,5 @@ // * Packages -import { TrashIcon, GripVerticalIcon, InfoIcon, Copy } from "lucide-react"; +import { TrashIcon, GripVerticalIcon, CheckIcon, CopyIcon } from "lucide-react"; import { usePage } from "@inertiajs/react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -19,22 +19,24 @@ import useTrans from "@/hooks/use-translations"; import str from "@/hooks/use-string"; import { useEffect, useState } from "react"; +/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ type SortableFieldProps = { - id: string; + id: number; index: number; field: DynamicField; fieldKey: string; - removeField: (id: string) => void; - updateField: (id: string, updatedField: Partial) => void; + removeField: (id: number) => void; + updateField: (id: number, updatedField: Partial) => void; required?: boolean; disabled?: boolean; getTransInput: ( field: "type" | "key" | "label" | "validation", - id: number + id: number, ) => string; transDelete: string; transDragAndDrop: string; }; +/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ export default function SortableField({ id, @@ -49,32 +51,47 @@ export default function SortableField({ transDelete, transDragAndDrop, }: SortableFieldProps) { - const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes; + // Enum. + const enumDynamicFieldTypes = usePage().props + .enumDynamicFieldTypes as DynamicFieldTypes; const getEnumDynamicFieldType = (value: number) => { - return Object.values(enumDynamicFieldTypes).find((def) => def.value === value); + return Object.values(enumDynamicFieldTypes).find( + (def) => def.value === value, + ); }; - const enumCurrent = getEnumDynamicFieldType(field.type)?.name + "_"; + // Key. + const enumCurrent = `${getEnumDynamicFieldType(field.type)?.name}_`; const splitAndGetEditableKey = (localeEditableKey: string): string => { const baseKeyArray = localeEditableKey.split(enumCurrent); return baseKeyArray[baseKeyArray.length - 1]; - } - const [editableKey, setEditableKey] = useState(splitAndGetEditableKey(field.key)) - const [isTypeChanged, setIsTypeChanged] = useState(false) - const completeKey = enumCurrent + editableKey + }; + const [editableKey, setEditableKey] = useState( + splitAndGetEditableKey(field.key), + ); + const [isTypeChanged, setIsTypeChanged] = useState(false); + const completeKey = enumCurrent + editableKey; + + // Copy. + const [copied, setCopied] = useState(false); + const handleClick = () => { + copyToClipBoard(completeKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; useEffect(() => { if (isTypeChanged) { - setEditableKey(splitAndGetEditableKey(editableKey)) + setEditableKey(splitAndGetEditableKey(editableKey)); } - setIsTypeChanged(false) - }, [isTypeChanged]) - - + setIsTypeChanged(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isTypeChanged]); useEffect(() => { - updateField(id, { key: completeKey }) - }, [fieldKey, editableKey, completeKey]) + updateField(id, { key: completeKey }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldKey, editableKey, completeKey]); const { attributes, @@ -92,8 +109,8 @@ export default function SortableField({ zIndex: isDragging ? 10 : 1, }; const copyToClipBoard = (data: string) => { - navigator.clipboard.writeText(data) - } + navigator.clipboard.writeText(data); + }; return (
} + content={ + + } tooltipContent={transDragAndDrop} className="cursor-grab h-full w-full" type="button" variant="outline" + // eslint-disable-next-line react/jsx-props-no-spreading {...attributes} + // eslint-disable-next-line react/jsx-props-no-spreading {...listeners} />
@@ -137,7 +158,9 @@ export default function SortableField({ label={getTransInput("label", index + 1)} placeholder="ex: John Doe" value={field.label} - setData={(_, value) => updateField(id, { label: value })} + setData={(_, value) => + updateField(id, { label: value }) + } />
{ - setEditableKey(value) - // updateField(id, { key: value }) + setEditableKey(value); }} />
- + - -

{str(useTrans('back.config_complete_label')).ucFirst().value()}

-

{completeKey} + +

+ {str( + useTrans( + "back.config_complete_label", + ), + ) + .ucFirst() + .value()} +

+

+ {completeKey}

-
{ - setIsTypeChanged(true) - updateField(id, { type: Number(value) }) + setIsTypeChanged(true); + updateField(id, { type: Number(value) }); }} required={required} disabled={disabled}