From b2a159a84f10948efea56d8a06fa576328f4f2a7 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 15 Dec 2025 15:18:23 +0100 Subject: [PATCH] feat: :sparkles: add model select component to the list of dynamic field types --- app/Enums/DynamicFieldTypes.php | 4 ++ .../Back/UpdateDynamicFieldRequest.php | 10 ++- lang/en/enums.php | 47 ++++++------- lang/fr/enums.php | 47 ++++++------- .../Components/DynamicPages/DynamicInputs.tsx | 67 +++++++++++++------ .../Components/Inputs/CompleteInputFile.tsx | 4 +- resources/ts/hooks/use-laravel-validation.tsx | 25 +++++++ 7 files changed, 133 insertions(+), 71 deletions(-) diff --git a/app/Enums/DynamicFieldTypes.php b/app/Enums/DynamicFieldTypes.php index 357791be..ec54e44f 100644 --- a/app/Enums/DynamicFieldTypes.php +++ b/app/Enums/DynamicFieldTypes.php @@ -23,6 +23,7 @@ enum DynamicFieldTypes: int case youtubeid = 12; case date = 13; case color = 14; + case model = 15; /** * Labels definition. @@ -44,6 +45,7 @@ enum DynamicFieldTypes: int self::youtubeid->name => 'enums.dynamic-field-types.youtubeid', self::date->name => 'enums.dynamic-field-types.date', self::color->name => 'enums.dynamic-field-types.color', + self::model->name => 'enums.dynamic-field-types.model', ]; /** @@ -64,6 +66,7 @@ enum DynamicFieldTypes: int self::youtubeid->name => 'required|string|size:11', self::date->name => 'required|date', self::color->name => 'required|string|hex_color', + self::model->name => 'required|numeric|exists:dynamic_pages,id', ]; /** @@ -96,5 +99,6 @@ enum DynamicFieldTypes: int self::youtubeid->name => false, self::date->name => false, self::color->name => false, + self::model->name => false, ]; } diff --git a/app/Http/Requests/Back/UpdateDynamicFieldRequest.php b/app/Http/Requests/Back/UpdateDynamicFieldRequest.php index 1427fac0..1204358d 100644 --- a/app/Http/Requests/Back/UpdateDynamicFieldRequest.php +++ b/app/Http/Requests/Back/UpdateDynamicFieldRequest.php @@ -44,8 +44,6 @@ abstract class UpdateDynamicFieldRequest extends FormRequest * Get the validation rules that apply to the request. * * @return array - * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - * @phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ public function rules(): array { @@ -170,6 +168,14 @@ abstract class UpdateDynamicFieldRequest extends FormRequest $ruleKey => $this->compileValidate(DynamicFieldTypes::color->getValidation(), $input) ]); break; + case DynamicFieldTypes::model->value: + // Set a new rule key for the model name. + $ruleKeyReserved = preg_replace('/([^.\s]+)$/', 'reserved_$1', $ruleKey); + $rules = array_merge($rules, [ + $ruleKey => $this->compileValidate(DynamicFieldTypes::model->getValidation(), $input), + $ruleKeyReserved => 'required|string|between:3,255', + ]); + break; default: throw new \UnexpectedValueException("Unrecognized dynamic type: " . ($input['type'] ?? 'null')); break; diff --git a/lang/en/enums.php b/lang/en/enums.php index 7e455d81..98ef022e 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -26,15 +26,16 @@ return [ 'youtubeid' => 'Youtube video identification', 'date' => 'Date', 'color' => 'Color', + 'model' => 'Model', ], 'contactFormType' => [ 'collaborator' => [ 'label' => 'join us as an employee', - 'slug' => 'join-us-as-an-employee' + 'slug' => 'join-us-as-an-employee' ], 'breeder' => [ 'label' => 'join us as a farmer', - 'slug' => 'join us as a farmer' + 'slug' => 'join us as a farmer' ], 'professional' => [ 'label' => 'a request as a professional' @@ -44,60 +45,60 @@ return [ ], 'export' => [ 'label' => 'other countries / Export', - 'slug' => 'export' + 'slug' => 'export' ], 'finance' => [ 'label' => 'finance', - 'slug' => 'finance-2' #FIXME: revoir le slug + 'slug' => 'finance-2' #FIXME: revoir le slug ], 'engagements' => [ 'label' => 'our commitments', - 'slug' => 'our-commitments-2' #FIXME: revoir le slug + 'slug' => 'our-commitments-2' #FIXME: revoir le slug ], 'product' => [ 'label' => 'product information', - 'slug' => 'product-information' + 'slug' => 'product-information' ], 'info' => [ 'label' => 'other general information', - 'slug' => 'other-general-information' + 'slug' => 'other-general-information' ], 'press' => [ 'label' => 'a press request' ], ], 'breedType' => [ - 'chicken' => 'Chickens / turkeys / guinea fowl', - 'duck' => 'Ducks', + 'chicken' => 'Chickens / turkeys / guinea fowl', + 'duck' => 'Ducks', 'chickenEggs' => 'Laying hens', - 'other' => 'Other' + 'other' => 'Other' ], 'customerAskingType' => [ 'collectiveRestauration' => 'Mass catering', 'commercialRestauration' => 'Commercial catering', - 'snaking' => 'Snacking', - 'gastronomy' => 'Gastronomy', - 'other' => 'Other', + 'snaking' => 'Snacking', + 'gastronomy' => 'Gastronomy', + 'other' => 'Other', ], - 'pressType' => [ + 'pressType' => [ 'groupLogo' => 'Group logo request', 'companyLogo' => 'Subsidiary/company logo request', 'brandLogo' => 'Brand logo request', 'photo' => 'Photo request', 'other' => 'Other' ], - 'news-category' => [ - 'news_rh' => 'HR news', - 'group_life' => 'Group life', - 'event' => 'Event', + 'news-category' => [ + 'news_rh' => 'HR news', + 'group_life' => 'Group life', + 'event' => 'Event', 'com_financial' => 'Financial Press Releases and Notices', - 'com' => 'Press release', - 'news_rse' => 'RSE news', - 'news_product' => 'Product news', + 'com' => 'Press release', + 'news_rse' => 'RSE news', + 'news_product' => 'Product news', 'press_article' => 'Article press', - 'part_sponso' => 'Partnerships and sponsorships' + 'part_sponso' => 'Partnerships and sponsorships' ], - 'news-status' => [ + 'news-status' => [ 'unpublished' => 'Unpublished', 'published' => 'Published', ], diff --git a/lang/fr/enums.php b/lang/fr/enums.php index 31ef5bbe..9c2cde63 100644 --- a/lang/fr/enums.php +++ b/lang/fr/enums.php @@ -26,60 +26,61 @@ return [ 'youtubeid' => 'Identifiant vidéo Youtube', 'date' => 'Date', 'color' => 'Couleur', + 'model' => 'Modèle', ], 'contactFormType' => [ 'collaborator' => [ 'label' => 'nous rejoindre en tant que collaborateur', - 'slug' => 'nous-rejoindre-en-tant-que-collaborateur' + 'slug' => 'nous-rejoindre-en-tant-que-collaborateur' ], 'breeder' => [ 'label' => 'nous rejoindre en tant qu’éleveur', - 'slug' => 'nous-rejoindre-en-tant-queleveur' + 'slug' => 'nous-rejoindre-en-tant-queleveur' ], 'professional' => [ 'label' => 'une demande en tant que professionnel' ], 'restauration' => [ - 'label' => 'restaurations, Distributeurs et Détaillants France', + 'label' => 'restaurations, Distributeurs et Détaillants France', 'subtitle' => 'une demande en tant que client / détaillant / distributeur', - 'slug' => 'une-demande-en-tant-que-client-detaillant-distributeur' + 'slug' => 'une-demande-en-tant-que-client-detaillant-distributeur' ], 'export' => [ 'label' => 'l’export', - 'slug' => 'lexport' + 'slug' => 'lexport' ], 'finance' => [ 'label' => 'la finance', - 'slug' => 'la-finance' + 'slug' => 'la-finance' ], 'commitments' => [ 'label' => 'nos engagements', - 'slug' => 'nos-engagements' + 'slug' => 'nos-engagements' ], 'product' => [ 'label' => 'une information produit', - 'slug' => 'une-information-produit' + 'slug' => 'une-information-produit' ], 'info' => [ 'label' => 'une autre information générale', - 'slug' => 'une-autre-information-generale' + 'slug' => 'une-autre-information-generale' ], 'press' => [ 'label' => 'une demande presse' ], ], 'breedType' => [ - 'chicken' => 'Poulets/dindes/pintades', - 'duck' => 'Canards', + 'chicken' => 'Poulets/dindes/pintades', + 'duck' => 'Canards', 'chickenEggs' => 'Poules pondeuses', - 'other' => 'Autre' + 'other' => 'Autre' ], 'customerAskingType' => [ 'collectiveRestauration' => 'Restauration collective', 'commercialRestauration' => 'Restauration commerciale', - 'snaking' => 'Snacking', - 'gastronomy' => 'Gastronomie', - 'other' => 'Autres', + 'snaking' => 'Snacking', + 'gastronomy' => 'Gastronomie', + 'other' => 'Autres', ], 'pressType' => [ 'groupLogo' => 'Demander le logo du Groupe', @@ -89,17 +90,17 @@ return [ 'other' => 'Autres' ], 'news-category' => [ - 'news_rh' => 'Actualités RH', - 'group_life' => 'Vie du Groupe', - 'event' => 'Événement', + 'news_rh' => 'Actualités RH', + 'group_life' => 'Vie du Groupe', + 'event' => 'Événement', 'com_financial' => 'Communiqués et Avis financiers', - 'com' => 'Communiqué', - 'news_rse' => 'Actualités RSE', - 'news_product' => 'Actualités Produits', + 'com' => 'Communiqué', + 'news_rse' => 'Actualités RSE', + 'news_product' => 'Actualités Produits', 'press_article' => 'Article de presse', - 'part_sponso' => 'Partenariats et sponsorings' + 'part_sponso' => 'Partenariats et sponsorings' ], - 'news-status' => [ + 'news-status' => [ 'unpublished' => 'Non publié', 'published' => 'Publié', ], diff --git a/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx b/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx index dfb6d475..3803e4d3 100644 --- a/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx +++ b/resources/ts/back/Components/DynamicPages/DynamicInputs.tsx @@ -1,10 +1,11 @@ // * Packages import { usePage } from "@inertiajs/react"; +import { useEffect, useRef } from "react"; // * Other import SimpleString from "@/back/Components/Inputs/SimpleString"; import SimpleNumber from "@/back/Components/Inputs/SimpleNumber"; import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch"; -import SimpleWysiwyg from "@/back/Components/Inputs/SimpleWysiwyg"; +import SimpleWysiwyg from "@/back/Components/Inputs/Wysiwyg/SimpleWysiwyg"; import CompleteInputFile from "@/back/Components/Inputs/CompleteInputFile"; import DynamicList from "@/back/Components/DynamicPages/DynamicList"; import SimpleTextarea from "@/back/Components/Inputs/SimpleTextarea"; @@ -12,17 +13,21 @@ import SimpleDatePicker from "@/back/Components/Inputs/SimpleDatePicker"; import SimpleColorPicker from "@/back/Components/Inputs/SimpleColorPicker"; import SimpleYoutubeId from "@/back/Components/Inputs/SimpleYoutubeId"; import SimpleVimeoId from "@/back/Components/Inputs/SimpleVimeoId"; +import ModelSelect from "@/back/Components/Inputs/ModelSelect"; import { ConfigField, DynamicFieldTypes, DynamicInternalField, + LaravelEnum, } from "@/back/types/global"; import str from "@/hooks/use-string"; import useTrans from "@/hooks/use-translations"; import { + extractExcludedIdFromExists, extractLaravelValidationValue, extractMinMaxFromValidation, extractRequiredValidation, + extractTableFromExists, } from "@/hooks/use-laravel-validation"; type DynamicInputsProps = { @@ -32,7 +37,7 @@ type DynamicInputsProps = { isChildren?: boolean; setData: ( name: string, - value: string | boolean | File | object | null, + value: string | number | boolean | File | object | null, ) => void; reset: (name?: string) => void; parentErrors?: Record | null; @@ -49,8 +54,8 @@ export default function DynamicInputs({ parentErrors, }: DynamicInputsProps) { // Enums. - const enumDynamicFieldTypes = usePage().props - .enumDynamicFieldTypes as DynamicFieldTypes; + const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes; + const enumElementPosition = usePage().props.enumElementPosition as LaravelEnum; // Translations. const transAssociateTo = useTrans("back.form_associate_to"); @@ -83,8 +88,7 @@ export default function DynamicInputs({ const fieldName = `${name}.${field.key}`; // Set the content value from the config key. - const fieldValue = - content?.[field.key as keyof typeof content] ?? ""; + const fieldValue = content?.[field.key as keyof typeof content] ?? ""; switch (field.type) { // String input. @@ -255,18 +259,7 @@ export default function DynamicInputs({ + ); + // Model select. + case enumDynamicFieldTypes.model.value: + // Extract only the model name from the validation. + const targetModel = extractTableFromExists(field.validation) || "dynamic_pages"; + + // Add a custom prefix in the last part of the name. + const newName = fieldName.replace(/([^.\s]+)$/, "reserved_$1"); + + // Set a state at false. + const initializedRef = useRef(false); + + // Set the model name in the custom key only once. + useEffect(() => { + if (initializedRef.current) return; + initializedRef.current = true; + + // If the value isn't already present in content. + if (content && !content[`reserved_${field.key}` as keyof typeof content]) { + setData(newName, targetModel); + } + }, [newName, targetModel]); + + return ( + ); default: diff --git a/resources/ts/back/Components/Inputs/CompleteInputFile.tsx b/resources/ts/back/Components/Inputs/CompleteInputFile.tsx index 0f9fd6cd..88ae7a77 100644 --- a/resources/ts/back/Components/Inputs/CompleteInputFile.tsx +++ b/resources/ts/back/Components/Inputs/CompleteInputFile.tsx @@ -282,8 +282,8 @@ export default function CompleteInputFile(props: CompleteInputFileProps) { )} } + variant="destructive" + content={} onClick={() => { setSelectedFile(null); setData(name, null); diff --git a/resources/ts/hooks/use-laravel-validation.tsx b/resources/ts/hooks/use-laravel-validation.tsx index ae4b009c..4b1c5b39 100644 --- a/resources/ts/hooks/use-laravel-validation.tsx +++ b/resources/ts/hooks/use-laravel-validation.tsx @@ -81,3 +81,28 @@ export const extractLaravelValidationValue = ( return null; }; + +/** + * Extract table from Laravel exists validation rule. + * Example: "required|numeric|exists:dynamic_pages,id" + * @param string validation + * @return boolean +*/ +export const extractTableFromExists = (rule: string): string | null => { + const match = rule.match(/exists:([^,]+)/); + if (!match) return null; + return match[1].trim() || null; +}; + +/** + * Extract excluded ID from a Laravel exists validation rule. + * Example: "required|numeric|exists:dynamic_pages,id,id,!5" + * returns: 5 + * @param rule string + * @return number | null + */ +export const extractExcludedIdFromExists = (rule: string): number | null => { + const match = rule.match(/exists:[^,]+,[^,]+,[^,]+,!([0-9]+)/); + if (!match) return null; + return Number(match[1]); +};