fix: 🎨 clean & comment dynamic pages components
This commit is contained in:
parent
ff23b486ce
commit
c6b2e8482a
6 changed files with 132 additions and 50 deletions
|
|
@ -100,6 +100,7 @@ return [
|
|||
"new_field" => "Add a new field",
|
||||
"delete_field" => "Delete this field",
|
||||
"dnd_field" => "Move this field",
|
||||
"list_children_needed" => "Children are required for this type :enumLabel",
|
||||
"empty_warning" => "No data recorded",
|
||||
"page_locked" => "This page is currently locked",
|
||||
"unsupported_type" => "Unsupported type:",
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ return [
|
|||
"new_field" => "Ajouter un nouveau champ",
|
||||
"delete_field" => "Supprimer ce champ",
|
||||
"dnd_field" => "Déplacer ce champ",
|
||||
"list_children_needed" => "Des enfants sont nécessaires pour ce type :enumLabel",
|
||||
"empty_warning" => "Aucune donnée enregistrée",
|
||||
"page_locked" => "Cette page est actuellement vérouillée",
|
||||
"unsupported_type" => "Type non supporté:",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type DynamicInputsProps = {
|
|||
reset: (name?: string) => void;
|
||||
};
|
||||
|
||||
// Allows to get specific input from enum value.
|
||||
export default function DynamicInputs({
|
||||
name,
|
||||
config,
|
||||
|
|
@ -31,6 +32,7 @@ export default function DynamicInputs({
|
|||
setData,
|
||||
reset,
|
||||
}: DynamicInputsProps) {
|
||||
// Enums.
|
||||
const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes;
|
||||
|
||||
// Translations.
|
||||
|
|
@ -49,12 +51,17 @@ export default function DynamicInputs({
|
|||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 items-start gap-4">
|
||||
{/* Loop on all config elements. */}
|
||||
{Object.entries(config).map(([id, field]) => {
|
||||
// Get the enum value from the element type value.
|
||||
const typeEnum = Object.values(enumDynamicFieldTypes).find(
|
||||
(def) => def.value === field.type,
|
||||
);
|
||||
|
||||
// Set the name of the current field.
|
||||
const fieldName = `${name}.${field.key}`;
|
||||
|
||||
// Set the content value from the config key.
|
||||
const fieldValue = content?.[field.key as keyof typeof content] ?? "";
|
||||
|
||||
switch (field.type) {
|
||||
|
|
@ -137,7 +144,7 @@ export default function DynamicInputs({
|
|||
label={getTrans(field.label)}
|
||||
setData={setData}
|
||||
reset={reset}
|
||||
value={String(fieldValue)}
|
||||
value={fieldValue as string | File | null}
|
||||
required={extractRequiredValidation(field.validation)}
|
||||
maxFileSize={extractLaravelValidationValue(field.validation, 'max')}
|
||||
allowedFormats={extractLaravelValidationValue(field.validation, 'mimes')}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// * Packages
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { GripVerticalIcon, TrashIcon } from "lucide-react";
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { closestCenter, DndContext, DragEndEvent, 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";
|
||||
|
|
@ -26,6 +26,7 @@ type DynamicListProps = {
|
|||
reset: (name?: string) => void;
|
||||
}
|
||||
|
||||
// Allows to get recursive list inputs.
|
||||
export default function DynamicList({
|
||||
name,
|
||||
parentName,
|
||||
|
|
@ -40,34 +41,44 @@ export default function DynamicList({
|
|||
if (!field.children && field.children!.length > 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Des enfants sont nécessaires pour ce type {enumLabel}
|
||||
{useTrans('back.list_children_needed', {
|
||||
params: { enumLabel }
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// Drag&Drop.
|
||||
// Drag & drop module for user interactions (mouse, touch, keyboard or other).
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
// Set local id.
|
||||
// Define a local identifier for each element to enable unique identification during drag and drop.
|
||||
const localValue = value.map((elmt, index) => {
|
||||
return { ...elmt, id: elmt.id ?? index + 1 };
|
||||
});
|
||||
|
||||
// Order all elements depending on order values.
|
||||
const sortedListItems = [...localValue].sort((a, b) => a.order - b.order);
|
||||
|
||||
// Extract some values from laravel rules validation.
|
||||
const isRequired = extractRequiredValidation(field.validation);
|
||||
const configLimitMax = extractMinMaxFromValidation(field.validation).max ?? 1;
|
||||
|
||||
/**
|
||||
* Get all elements without each unique identification values.
|
||||
* @param fields DynamicInternalField[]
|
||||
* @returns Omit<DynamicInternalField, 'id'>[]
|
||||
*/
|
||||
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 it is an array, we treat it recursively.
|
||||
if (Array.isArray(value)) {
|
||||
newObj[key] = removeIds(value);
|
||||
}
|
||||
// Sinon, on garde la valeur telle quelle
|
||||
// Otherwise, we keep the value as it is.
|
||||
else {
|
||||
newObj[key] = value;
|
||||
}
|
||||
|
|
@ -76,6 +87,69 @@ export default function DynamicList({
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a specific element from the list.
|
||||
* @param parentName string
|
||||
* @param fieldKey string
|
||||
* @param index number
|
||||
* @return void
|
||||
*/
|
||||
const handleRemoveField = (parentName: string, fieldKey: string, index: number) => {
|
||||
// Get the current list of elements.
|
||||
const list = Array.isArray(content?.[fieldKey as keyof typeof content])
|
||||
// @ts-ignore
|
||||
? [...content[fieldKey as keyof typeof content]]
|
||||
: [];
|
||||
// Remove the targeted element.
|
||||
list.splice(index, 1);
|
||||
// Save the new list updated.
|
||||
setData(`${parentName}.${fieldKey}`, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder the elements list.
|
||||
* @param event DragEndEvent
|
||||
* @return void
|
||||
*/
|
||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
// Check the unique identification value.
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
// Get the two elements that are switchable by drag and drop.
|
||||
const oldIndex = sortedListItems.findIndex((i) => i.id === active.id);
|
||||
const newIndex = sortedListItems.findIndex((i) => i.id === over.id);
|
||||
|
||||
// Reorder all elements list to update visually all elements.
|
||||
const reorderedList = arrayMove(sortedListItems, oldIndex, newIndex)
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
order: index + 1,
|
||||
}));
|
||||
|
||||
// Save the new ordered list elements.
|
||||
setData(`${parentName}.${field.key}`, reorderedList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new field to the list.
|
||||
* @return void
|
||||
*/
|
||||
const handleAddNewField = () => {
|
||||
// Set new element with unique identification and order.
|
||||
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 };
|
||||
|
||||
// Save the list with the new element.
|
||||
setData(`${parentName}.${field.key}.${newIndex}`, newItemWithOrder);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<Label htmlFor={parentName}>{field.label + (isRequired ? " *" : "")}</Label>
|
||||
|
|
@ -85,29 +159,24 @@ export default function DynamicList({
|
|||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
{/*
|
||||
The main provider for dnd-kit.
|
||||
It manages the entire drag-and-drop system: sensors, movement, collision detection, and lifecycle callbacks (onDragStart, onDragEnd, etc.).
|
||||
*/}
|
||||
<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);
|
||||
}}
|
||||
onDragEnd={handleOnDragEnd}
|
||||
>
|
||||
{/*
|
||||
Defines a reorderable list area.
|
||||
It registers sortable items and applies the chosen sorting strategy (vertical, horizontal, grid) to handle item repositioning.
|
||||
*/}
|
||||
<SortableContext
|
||||
items={sortedListItems.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{/* Use the recursive component for each elements. */}
|
||||
{sortedListItems.map((item, itemIndex) => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
|
|
@ -119,30 +188,18 @@ export default function DynamicList({
|
|||
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);
|
||||
}}
|
||||
removeField={handleRemoveField}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</Accordion>
|
||||
)}
|
||||
<AddNewFieldBtn array={sortedListItems} limitMax={configLimitMax} 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);
|
||||
}} />
|
||||
<AddNewFieldBtn
|
||||
array={sortedListItems}
|
||||
limitMax={configLimitMax}
|
||||
onClick={handleAddNewField}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -159,6 +216,7 @@ type SortableItemProps = {
|
|||
removeField: (parentName: string, fieldKey: string, index: number) => void;
|
||||
}
|
||||
|
||||
// Recursive component.
|
||||
function SortableItem({
|
||||
id,
|
||||
item,
|
||||
|
|
@ -170,8 +228,15 @@ function SortableItem({
|
|||
reset,
|
||||
removeField,
|
||||
}: SortableItemProps) {
|
||||
// Provides the necessary props and helpers to make this item draggable and reorderable.
|
||||
// Includes event listeners, accessibility attributes, DOM ref, drag transform/transition values,
|
||||
// and a flag indicating whether the item is dragging.
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||
|
||||
// Style applied to the sortable item:
|
||||
// - handles drag movement (transform),
|
||||
// - animations (transition),
|
||||
// - and visual feedback (opacity, zIndex) while dragging.
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
|
|
@ -190,8 +255,8 @@ function SortableItem({
|
|||
<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">
|
||||
{/* Define a drag-and-drop button to swap the order between the current element and another one. */}
|
||||
<div className="flex-grow flex items-stretch">
|
||||
{/* Drag handle */}
|
||||
<BtnOptionnalLinkWithTooltip
|
||||
content={<GripVerticalIcon className="w-4 h-4 m-auto" />}
|
||||
tooltipContent={transDragAndDrop}
|
||||
|
|
@ -203,6 +268,7 @@ function SortableItem({
|
|||
{...attributes}
|
||||
/>
|
||||
</div>
|
||||
{/* Define a delete button to destroy the current element. */}
|
||||
<BtnOptionnalLinkWithTooltip
|
||||
content={<TrashIcon className="w-4 h-4" />}
|
||||
tooltipContent={transDelete}
|
||||
|
|
@ -212,6 +278,7 @@ function SortableItem({
|
|||
onClick={() => removeField(parentName, field.key, itemIndex)}
|
||||
/>
|
||||
</div>
|
||||
{/* Define the trigger button to open or close the current accordion. */}
|
||||
<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}
|
||||
|
|
@ -219,6 +286,7 @@ function SortableItem({
|
|||
</div>
|
||||
</div>
|
||||
<AccordionContent className="pt-4 pb-1 px-1">
|
||||
{/* Get specific input from the config value. */}
|
||||
<DynamicInputs
|
||||
name={`${name}.${itemIndex}`}
|
||||
config={field.children}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,14 @@ type DynamicStatusBadgeProps = {
|
|||
value: number;
|
||||
};
|
||||
|
||||
// Get a stylised badge for a specific enum value.
|
||||
export default function DynamicStatusBadge({ value }: DynamicStatusBadgeProps) {
|
||||
// Get all enum values, labels and names.
|
||||
const { enumDynamicPageStatus } = usePage().props as unknown as {
|
||||
enumDynamicPageStatus: LaravelEnum;
|
||||
};
|
||||
|
||||
// Set specific icon and style classes for each enum value.
|
||||
const statusStyles = {
|
||||
[enumDynamicPageStatus.draft.value]: {
|
||||
color: "bg-gray-200 dark:bg-gray-900",
|
||||
|
|
@ -28,15 +32,16 @@ export default function DynamicStatusBadge({ value }: DynamicStatusBadgeProps) {
|
|||
Icon: CheckIcon,
|
||||
},
|
||||
};
|
||||
|
||||
// Get the current enum value.
|
||||
const enumValue = Object.values(enumDynamicPageStatus).find(
|
||||
(status) => status.value === value,
|
||||
);
|
||||
|
||||
// Get the color and icon for the current enum value.
|
||||
const { color, Icon } = statusStyles[
|
||||
enumValue?.value as keyof typeof statusStyles
|
||||
] ?? {
|
||||
color: "bg-green-200 dark:bg-green-900",
|
||||
Icon: CheckIcon,
|
||||
};
|
||||
];
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`border-0 ${color}`}>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ import {
|
|||
DialogFooter,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
// * Other
|
||||
import SimpleString from "@/back/Components/Inputs/SimpleString";
|
||||
import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch";
|
||||
|
|
@ -31,11 +36,6 @@ import str from "@/hooks/use-string";
|
|||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Methods } from "@/lib/enums";
|
||||
import { AppEnv, AuthProp, ConfigField, DynamicPageModel, LaravelEnum, PageConfigModel } from "@/back/types/global";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function Form({
|
||||
model,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue