Table des matières

Extrafields et formulaires dynamiques

Les extrafields Dolibarr permettent d'ajouter des champs personnalisés aux objets métier. SmartMaker offre un système complet pour les exposer à l'application React avec génération automatique de formulaires.

Vue d'ensemble

Le système extrafields SmartMaker fonctionne en trois étapes :

1. Admin Dolibarr : Configure quels extrafields exposer (RO/RW)
              ↓
2. Mapper PHP : Lit la configuration, expose les métadonnées
              ↓
3. React : Génère dynamiquement les formulaires

1. Configuration admin

Page de configuration

SmartBoot génère une page d'administration admin/smartmaker.php qui permet de configurer les extrafields exposés à l'application.

snippet.php
// Définir les objets Dolibarr à configurer
$elementsToConfig = array(
    'fichinter' => 'Interventions',
    'societe' => 'Tiers',
    'projet_task' => 'Tâches',
);

Cette page affiche tous les extrafields de chaque objet avec deux colonnes :

Option Description
RO (Read-Only) Le champ est visible dans l'application mais non modifiable
RW (Read-Write) Le champ est visible ET modifiable

Stockage de la configuration

La configuration est stockée dans les constantes Dolibarr :

MONMODULE_SMARTMAKER_EXTRAFIELDS_RO = "champ1,champ2,champ3"
MONMODULE_SMARTMAKER_EXTRAFIELDS_RW = "champ4,champ5"

2. Mapper PHP (dmGenericObject)

Structure de base

Le mapper hérite de dmBase et utilise dmTrait pour la gestion des extrafields :

snippet.php
<?php
namespace MonModule\Api;
 
use SmartAuth\DolibarrMapping\dmBase;
use SmartAuth\DolibarrMapping\dmTrait;
 
class dmItem extends dmBase
{
    use dmTrait;
 
    // Classe Dolibarr source
    protected $parentClassName = 'Fichinter';
 
    // Element pour les extrafields (table llx_extrafields)
    protected $parentElementToUseForExtraFields = 'fichinter';
 
    // Mapping des champs standards
    protected $listOfPublishedFields = [
        'rowid'       => 'id',
        'ref'         => 'ref',
        'description' => 'description',
        'fk_statut'   => 'status',
    ];
 
    // Champs modifiables via API
    protected $writableFields = [];
}

Chargement automatique des extrafields

Le constructeur charge la configuration depuis Dolibarr :

snippet.php
public function __construct()
{
    global $db;
    $this->db = $db;
 
    // Extrafields en lecture seule
    $extRO = getDolGlobalString('MONMODULE_SMARTMAKER_EXTRAFIELDS_RO');
    if (!empty($extRO)) {
        foreach (explode(',', $extRO) as $field) {
            $field = trim($field);
            if (!empty($field)) {
                $key = 'options_' . $field;
                $this->listOfPublishedFields[$key] = $key;
            }
        }
    }
 
    // Extrafields en lecture-écriture
    $extRW = getDolGlobalString('MONMODULE_SMARTMAKER_EXTRAFIELDS_RW');
    if (!empty($extRW)) {
        foreach (explode(',', $extRW) as $field) {
            $field = trim($field);
            if (!empty($field)) {
                $key = 'options_' . $field;
                $this->listOfPublishedFields[$key] = $key;
                $this->writableFields[] = $key;
            }
        }
    }
 
    $this->boot();
}

Méthode objectDesc()

La méthode objectDesc() (fournie par dmTrait) retourne les métadonnées des champs pour le frontend :

snippet.php
// Dans le controller
$mapping = new dmItem();
$config = $mapping->objectDesc();
 
// Retourne un tableau avec les infos de chaque champ :
// [
//     'options_niveau' => [
//         'type' => 'select',
//         'label' => 'Niveau',
//         'required' => true,
//         'options' => ['1' => 'Facile', '2' => 'Moyen', '3' => 'Difficile'],
//         'writable' => true,
//     ],
//     ...
// ]

3. Controller : Exposer la configuration

Le controller doit retourner la configuration au frontend :

snippet.php
class ItemController
{
    private $mapping;
 
    public function __construct()
    {
        $this->mapping = new dmItem();
    }
 
    // GET /items (liste + config)
    public function index($arr = null)
    {
        // ... charger les items ...
 
        return [[
            'statusCode' => 200,
            'items' => $items,
            'config' => $this->mapping->objectDesc(),  // Métadonnées des champs
        ], 200];
    }
 
    // GET /items/123 (détail + config)
    public function show($arr = null)
    {
        $id = $arr['id'] ?? 0;
        // ... charger l'item ...
 
        return [[
            'statusCode' => 200,
            'item' => $this->mapping->map($dolibarrObject),
            'config' => $this->mapping->objectDesc(),
        ], 200];
    }
}

4. Frontend : Formulaires dynamiques

Stockage de la configuration

Stocker la configuration reçue de l'API dans Redux ou le state local :

snippet.jsx
// Dans le composant ou via Redux
const [config, setConfig] = useState({});
 
useEffect(() => {
    api.private.get('items')
        .then(response => {
            setConfig(response.config || {});
            // ... traiter les items ...
        });
}, []);

FormComponentsMap

SmartCommon fournit FormComponentsMap qui mappe les types Dolibarr vers les composants React :

snippet.jsx
import { FormComponentsMap } from "@cap-rel/smartcommon";
 
// FormComponentsMap retourne le composant approprié selon le type :
// 'varchar'  → Input
// 'text'     → Textarea
// 'int'      → Input type="number"
// 'double'   → Input type="number" step="0.01"
// 'date'     → DatePicker
// 'datetime' → DateTimePicker
// 'boolean'  → Checkbox
// 'select'   → Select
// 'sellist'  → Select (avec options depuis la DB)
// 'radio'    → RadioGroup
// 'checkbox' → CheckboxGroup
// 'link'     → SearchSelect (lien vers autre objet)

Génération dynamique du formulaire

snippet.jsx
const DynamicForm = ({ config, values, onChange }) => {
    return (
        <div className="flex flex-col gap-4">
            {Object.entries(config).map(([fieldName, fieldConfig]) => {
                // Récupérer le composant selon le type
                const Component = FormComponentsMap(fieldConfig.type);
 
                if (!Component) return null;
 
                return (
                    <Component
                        key={fieldName}
                        label={fieldConfig.label}
                        value={values[fieldName] || ''}
                        onChange={(val) => onChange(fieldName, val)}
                        required={fieldConfig.required}
                        disabled={!fieldConfig.writable}
                        options={fieldConfig.options}  // Pour select/radio
                    />
                );
            })}
        </div>
    );
};

Exemple complet avec useApi

snippet.jsx
import { useStates, useApi, Page, Button, Block } from "@cap-rel/smartcommon";
import { FormComponentsMap } from "@cap-rel/smartcommon";
 
export const ItemEditPage = ({ itemId }) => {
    const { states, set } = useStates({
        item: null,
        config: {},
        isLoading: true,
        isSaving: false,
    });
 
    const { item, config, isLoading, isSaving } = states;
    const api = useApi();
 
    // Charger l'item et sa configuration
    useEffect(() => {
        api.private.get(`items/${itemId}`)
            .then(response => {
                set("item", response.item);
                set("config", response.config);
            })
            .finally(() => set("isLoading", false));
    }, [itemId]);
 
    // Modifier une valeur
    const handleFieldChange = (fieldName, value) => {
        set("item", { ...item, [fieldName]: value });
    };
 
    // Sauvegarder
    const handleSave = () => {
        set("isSaving", true);
        api.private.put(`items/${itemId}`, item)
            .then(() => {
                // Succès
            })
            .finally(() => set("isSaving", false));
    };
 
    if (isLoading) return <div>Chargement...</div>;
 
    return (
        <Page>
            <Block title="Modifier l'item">
                {/* Champs standards */}
                <Input
                    label="Référence"
                    value={item?.ref || ''}
                    disabled
                />
 
                {/* Extrafields dynamiques */}
                {Object.entries(config).map(([fieldName, fieldConfig]) => {
                    // Ne montrer que les extrafields (commencent par options_)
                    if (!fieldName.startsWith('options_')) return null;
 
                    const Component = FormComponentsMap(fieldConfig.type);
                    if (!Component) return null;
 
                    return (
                        <Component
                            key={fieldName}
                            label={fieldConfig.label}
                            value={item?.[fieldName] || ''}
                            onChange={(val) => handleFieldChange(fieldName, val)}
                            disabled={!fieldConfig.writable}
                            required={fieldConfig.required}
                            options={fieldConfig.options}
                        />
                    );
                })}
 
                <Button
                    onClick={handleSave}
                    loading={isSaving}
                >
                    Enregistrer
                </Button>
            </Block>
        </Page>
    );
};

Types d'extrafields supportés

Type Dolibarr Composant React Notes
varchar Input Texte court
text Textarea Texte long
int Input (number) Entier
double Input (number) Décimal avec step
date DatePicker Date seule
datetime DateTimePicker Date et heure
boolean Checkbox Oui/Non
select Select Liste déroulante
sellist Select Liste depuis SQL
radio RadioGroup Boutons radio
checkbox CheckboxGroup Cases à cocher multiples
link SearchSelect Lien vers objet Dolibarr
price Input (number) Prix avec formatage

Bonnes pratiques

Sécurité

Performance

UX

Résumé

Le système extrafields SmartMaker permet de :

  1. Configurer via l'admin Dolibarr quels champs exposer
  2. Mapper automatiquement avec le constructeur dmGenericObject
  3. Exposer les métadonnées via objectDesc()
  4. Générer dynamiquement les formulaires React avec FormComponentsMap

Ce système évite de modifier le code à chaque ajout d'extrafield : la configuration admin suffit.

← Mappers | Module 9 : Intégration →