fix: 🐛 set id locally for each inputs in page configuration
This commit is contained in:
parent
2c819672a8
commit
a67490f997
4 changed files with 126 additions and 63 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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<InternalField[]>(() =>
|
||||
(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<DynamicField>
|
||||
* @returns void
|
||||
*/
|
||||
const updateField = (fieldId: string, updatedField: Partial<DynamicField>): void => {
|
||||
const updateField = (
|
||||
fieldId: number,
|
||||
updatedField: Partial<DynamicField>,
|
||||
): 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) => (
|
||||
<SortableField
|
||||
key={field.id}
|
||||
fieldKey={field.key}
|
||||
id={field.id}
|
||||
fieldKey={field.key}
|
||||
index={index}
|
||||
field={field}
|
||||
removeField={() => 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")}
|
||||
</Button>
|
||||
</div>
|
||||
<InputError message={usePage().props.errors?.[name]} />
|
||||
<InputError message={errors?.[name]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DynamicField>) => void;
|
||||
removeField: (id: number) => void;
|
||||
updateField: (id: number, updatedField: Partial<DynamicField>) => 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 (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
|
|
@ -105,12 +122,16 @@ export default function SortableField({
|
|||
{/* Drag button */}
|
||||
<div className="flex-grow flex items-stretch">
|
||||
<BtnOptionnalLinkWithTooltip
|
||||
content={<GripVerticalIcon className="w-4 h-4 m-auto" />}
|
||||
content={
|
||||
<GripVerticalIcon className="w-4 h-4 m-auto" />
|
||||
}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -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 })
|
||||
}
|
||||
/>
|
||||
<div className="flex items-end gap-2">
|
||||
<SimpleString
|
||||
|
|
@ -146,22 +169,41 @@ export default function SortableField({
|
|||
placeholder="ex: John Doe"
|
||||
value={editableKey}
|
||||
setData={(_, value) => {
|
||||
setEditableKey(value)
|
||||
// updateField(id, { key: value })
|
||||
setEditableKey(value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className="text-blue-500 size-4" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<CopyIcon
|
||||
className={`size-4 ${copied ? "hidden" : "block"}`}
|
||||
/>
|
||||
<CheckIcon
|
||||
className={`size-4 ${copied ? "block" : "hidden"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<h4 className="font-semibold">{str(useTrans('back.config_complete_label')).ucFirst().value()}</h4>
|
||||
<p className="text-muted-foreground mt-1">{completeKey}
|
||||
<TooltipContent className="text-center">
|
||||
<h4 className="font-semibold">
|
||||
{str(
|
||||
useTrans(
|
||||
"back.config_complete_label",
|
||||
),
|
||||
)
|
||||
.ucFirst()
|
||||
.value()}
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{completeKey}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button type="button" variant={"ghost"} size={"icon"} onClick={() => copyToClipBoard(completeKey)}><Copy className="size-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
<EnumSelect
|
||||
|
|
@ -171,8 +213,8 @@ export default function SortableField({
|
|||
selected={String(field.type)}
|
||||
options={enumDynamicFieldTypes}
|
||||
setData={(_, value) => {
|
||||
setIsTypeChanged(true)
|
||||
updateField(id, { type: Number(value) })
|
||||
setIsTypeChanged(true);
|
||||
updateField(id, { type: Number(value) });
|
||||
}}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue