ldc-groupe/resources/ts/back/Components/DynamicPages/DynamicList.tsx
2025-11-07 19:00:09 +01:00

234 lines
No EOL
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// * Packages
import { CSS } from "@dnd-kit/utilities";
import { GripVerticalIcon, TrashIcon } from "lucide-react";
import { closestCenter, DndContext, 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";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
// * Other
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import DynamicInputs from "@/back/Components/DynamicPages/DynamicInputs";
import useTrans from "@/hooks/use-translations";
import { extractRequiredValidation } from "@/hooks/use-laravel-validation";
import AddNewFieldBtn from "@/back/Components//PageConfigs/AddNewFieldBtn";
import { ConfigField, DynamicInternalField } from "@/back/types/global";
type DynamicListProps = {
name: string,
parentName: string,
value: DynamicInternalField[],
enumLabel: string,
field: ConfigField,
content: DynamicInternalField | null;
setData: (name: string, value: string | boolean | File | object | null) => void;
reset: (name?: string) => void;
}
export default function DynamicList({
name,
parentName,
value,
enumLabel,
field,
content,
setData,
reset,
}: DynamicListProps) {
// Notice user there must be children config.
if (!field.children && field.children!.length > 0) {
return (
<p className="text-muted-foreground text-sm">
Des enfants sont nécessaires pour ce type {enumLabel}
</p>
);
}
// Drag&Drop.
const sensors = useSensors(useSensor(PointerSensor));
// Set local id.
const localValue = value.map((elmt, index) => {
return { ...elmt, id: elmt.id ?? index + 1 };
});
const sortedListItems = [...localValue].sort((a, b) => a.order - b.order);
const isRequired = extractRequiredValidation(field.validation);
const removeIds = (fields: DynamicInternalField[]): Omit<DynamicInternalField, 'id'>[] => {
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 (Array.isArray(value)) {
newObj[key] = removeIds(value);
}
// Sinon, on garde la valeur telle quelle
else {
newObj[key] = value;
}
}
return newObj;
});
};
return (
<div className="col-span-1 md:col-span-2">
<Label htmlFor={parentName}>{field.label + (isRequired ? " *" : "")}</Label>
{value.length > 0 && (
<Accordion
type="single"
collapsible
className="w-full"
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(event) => {
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);
}}
>
<SortableContext
items={sortedListItems.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
{sortedListItems.map((item, itemIndex) => (
<SortableItem
key={item.id}
id={item.id}
item={item}
itemIndex={itemIndex}
name={name}
parentName={parentName}
field={field}
setData={setData}
reset={reset}
removeField={(parentName: string, fieldKey: string, index: number) => {
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);
}}
/>
))}
</SortableContext>
</DndContext>
</Accordion>
)}
<AddNewFieldBtn array={sortedListItems} onClick={() => {
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);
}} />
</div>
)
}
type SortableItemProps = {
id: number;
item: any;
itemIndex: number;
name: string;
parentName: string;
field: any;
setData: (name: string, value: string | boolean | File | object | null) => void;
reset: (name?: string) => void;
removeField: (parentName: string, fieldKey: string, index: number) => void;
}
function SortableItem({
id,
item,
itemIndex,
name,
parentName,
field,
setData,
reset,
removeField,
}: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 50 : 0,
};
// Translations.
const transDragAndDrop = useTrans("back.dnd_field");
const transDelete = useTrans("back.delete_field");
return (
<div ref={setNodeRef} style={style} className="relative mb-3">
<AccordionItem value={`item-${itemIndex}`}>
<Card className="errors-classes">
<CardContent className="flex flex-col p-3">
<div className="flex flex-col lg:flex-row-reverse gap-1.5">
<div className="flex flex-row gap-1.5 w-full h-full">
<div className="flex-grow flex items-stretch">
{/* Drag handle */}
<BtnOptionnalLinkWithTooltip
content={<GripVerticalIcon className="w-4 h-4 m-auto" />}
tooltipContent={transDragAndDrop}
className="cursor-grab h-full w-full"
type="button"
variant="outline"
size="icon"
{...listeners}
{...attributes}
/>
</div>
<BtnOptionnalLinkWithTooltip
content={<TrashIcon className="w-4 h-4" />}
tooltipContent={transDelete}
type="button"
variant="destructive"
size="icon"
onClick={() => removeField(parentName, field.key, itemIndex)}
/>
</div>
<div className="w-full lg:w-2/3 xl:w-1/2">
<AccordionTrigger className="inline-flex items-center justify-between gap-2 whitespace-nowrap rounded-md text-sm font-medium bg-secondary text-secondary-foreground hover:bg-secondary/80 h-10 px-4 py-2 w-full">
Élément n°{itemIndex + 1}
</AccordionTrigger>
</div>
</div>
<AccordionContent className="pt-4 pb-1 px-1">
<DynamicInputs
name={`${name}.${itemIndex}`}
config={field.children}
content={item}
setData={setData}
reset={reset}
/>
</AccordionContent>
</CardContent>
</Card>
</AccordionItem>
</div>
);
}