feat: ✨ add model select component to the list of dynamic field types

This commit is contained in:
Alexis Gousseau 2025-12-15 15:18:23 +01:00
parent 340e53929c
commit b2a159a84f
7 changed files with 133 additions and 71 deletions

View file

@ -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,
];
}

View file

@ -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;

View file

@ -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',
],

View file

@ -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é',
],

View file

@ -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<string, string> | 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({
<CompleteInputFile
key={id}
name={fieldName}
label={str(
useTrans("back.form_associate_to", {
params: {
attribute: useTrans(
"validation.custom.file",
),
},
choice: 1,
}),
)
.ucFirst()
.value()}
label={getTrans(field.label)}
setData={setData}
reset={reset}
value={fieldValue as string | File | null}
@ -327,9 +320,41 @@ export default function DynamicInputs({
setData={setData}
value={String(fieldValue)}
label={getTrans(field.label)}
required={extractRequiredValidation(
field.validation,
)}
required={extractRequiredValidation(field.validation)}
/>
);
// 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 (
<ModelSelect
name={fieldName}
targetModel={targetModel}
label={getTrans(field.label)}
exclude={extractExcludedIdFromExists(field.validation)}
placeholder="ex: John Doe"
selected={Number(fieldValue)}
required={extractRequiredValidation(field.validation)}
setData={setData}
/>
);
default:

View file

@ -282,8 +282,8 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
)}
<BtnOptionnalLinkWithTooltip
className="absolute top-2 right-2"
variant={'destructive'}
content={<Trash />}
variant="destructive"
content={<Trash className="w-4 h-4 m-auto" />}
onClick={() => {
setSelectedFile(null);
setData(name, null);

View file

@ -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]);
};