Version de développement
Cette documentation décrit la version « next » (develop), non encore publiée. Pour la version stable, basculez sur « v5 » dans l'en-tête.
Développement

Composants UI Espresso

Guide de développement pour les composants UI du thème Espresso dans Dokos

Composants UI Espresso

Le thème Espresso introduit une série de composants UI standardisés pour harmoniser l'apparence et le comportement de l'interface de Dokos. Ces composants sont conçus pour être modulaires, accessibles et performants, tout en supportant des fonctionnalités avancées comme le chargement asynchrone des options.

Ce guide s'adresse aux développeurs et administrateurs techniques qui souhaitent personnaliser ou étendre l'interface de Dokos en utilisant les composants Espresso.

Principes de base

Cohérence visuelle

Tous les composants Espresso partagent les mêmes variables de design (couleurs, espacements, typographie) et respectent les normes d'accessibilité (contrastes, tailles de cible tactiles, navigation au clavier).

Modularité

Les composants sont conçus pour être réutilisables et configurables. Par exemple, un même composant Button peut être utilisé comme bouton principal, secondaire ou de danger, simplement en changeant sa propriété variant.

Performance

Les composants Espresso sont optimisés pour minimiser leur empreinte mémoire et leur temps de rendu. Ils supportent nativement le chargement asynchrone des données, ce qui permet de réduire le temps de chargement initial de l'interface.

Composants disponibles

Le composant Dropdown permet d'afficher une liste d'options dans un menu déroulant. Il supporte les options statiques et asynchrones, ainsi que les groupes d'options et les icônes.

Propriétés

PropriétéTypeDescriptionValeur par défaut
optionsArrayListe des options statiques (remplacé par asyncOptions si fourni)[]
asyncOptionsFunctionFonction asynchrone retournant une promesse résolue avec les optionsnull
placeholderStringTexte affiché lorsque aucune option n'est sélectionnée"Sélectionner une option"
iconStringIcône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*)null
disabledBooleanDésactive le menu déroulantfalse
searchableBooleanActive la recherche dans les optionsfalse

Exemple d'utilisation

// Menu déroulant avec options statiques
const dropdown = new frappe.ui.Dropdown({
    parent: cur_frm.fields_dict["custom_field"].$wrapper,
    options: [
        { label: "Option 1", value: "1" },
        { label: "Option 2", value: "2" }
    ],
    placeholder: "Choisir une option"
});

// Menu déroulant avec options asynchrones
const asyncDropdown = new frappe.ui.Dropdown({
    parent: cur_frm.fields_dict["custom_field"].$wrapper,
    asyncOptions: async () => {
        const options = await frappe.call({
            method: "dokos.api.get_dynamic_options",
            args: { doctype: cur_frm.doctype }
        });
        return options.message;
    },
    placeholder: "Chargement en cours…"
});

Bonnes pratiques

  • Préchargez les options critiques : Si certaines options sont fréquemment utilisées, envisagez de les précharger pour éviter un délai de chargement.
  • Gérez les erreurs : Prévoyez un retour utilisateur clair en cas d'échec du chargement asynchrone.
  • Limitez le nombre d'options : Pour les listes longues, activez la recherche (searchable: true) ou utilisez une pagination côté serveur.

ContextMenu (Menu contextuel)

Le composant ContextMenu permet d'afficher un menu contextuel lorsqu'un utilisateur effectue un clic droit ou clique sur un bouton dédié. Il est couramment utilisé pour les actions rapides sur les documents ou les lignes de grille.

Propriétés

PropriétéTypeDescriptionValeur par défaut
targetHTMLElementÉlément DOM sur lequel le menu contextuel est attachénull
itemsArrayListe des éléments statiques du menu[]
asyncItemsFunctionFonction asynchrone retournant une promesse résolue avec les éléments du menunull
onClickFunctionCallback appelé lorsqu'un élément est cliquénull

Exemple d'utilisation

// Menu contextuel avec éléments statiques
const contextMenu = new frappe.ui.ContextMenu({
    target: cur_frm.fields_dict["items"].grid.wrapper,
    items: [
        { label: "Modifier", action: () => cur_frm.trigger("edit_row") },
        { label: "Supprimer", action: () => cur_frm.trigger("delete_row") }
    ]
});

// Menu contextuel avec éléments asynchrones
const asyncContextMenu = new frappe.ui.ContextMenu({
    target: cur_frm.fields_dict["items"].grid.wrapper,
    asyncItems: async () => {
        const items = await frappe.call({
            method: "dokos.api.get_contextual_actions",
            args: {
                doctype: cur_frm.doctype,
                docname: cur_frm.docname
            }
        });
        return items.message;
    }
});

Bonnes pratiques

  • Contexte utilisateur : Adaptez les éléments du menu en fonction des permissions de l'utilisateur.
  • Feedback visuel : Affichez un indicateur de chargement si le menu met du temps à apparaître.
  • Sécurité : Validez toujours les actions côté serveur pour éviter les exécutions non autorisées.

Button (Bouton)

Le composant Button est utilisé pour déclencher des actions. Il supporte plusieurs variantes (primaire, secondaire, danger) et peut inclure des icônes ou des badges.

Propriétés

PropriétéTypeDescriptionValeur par défaut
labelStringTexte du bouton"Bouton"
variantStringVariante du bouton (primary, secondary, danger, ghost)"secondary"
iconStringIcône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*)null
disabledBooleanDésactive le boutonfalse
onClickFunctionCallback appelé lorsque le bouton est cliquénull
loadingBooleanAffiche un indicateur de chargementfalse

Exemple d'utilisation

// Bouton primaire avec icône
const primaryButton = new frappe.ui.Button({
    parent: cur_frm.fields_dict["actions"].$wrapper,
    label: "Valider",
    variant: "primary",
    icon: "i-mdi-check",
    onClick: () => cur_frm.save()
});

// Bouton de danger avec état de chargement
const dangerButton = new frappe.ui.Button({
    parent: cur_frm.fields_dict["actions"].$wrapper,
    label: "Supprimer",
    variant: "danger",
    onClick: async () => {
        dangerButton.setLoading(true);
        await frappe.call({
            method: "dokos.api.delete_document",
            args: { doctype: cur_frm.doctype, docname: cur_frm.docname }
        });
        dangerButton.setLoading(false);
    }
});

Bonnes pratiques

  • Variantes : Utilisez la variante primary pour les actions principales, secondary pour les actions secondaires, et danger pour les actions destructrices.
  • Feedback : Activez l'état loading pendant les opérations asynchrones pour informer l'utilisateur.
  • Accessibilité : Assurez-vous que le texte du bouton est clair et concis (ex. : "Valider" plutôt que "OK").

Badge (Badge)

Le composant Badge est utilisé pour afficher des indicateurs visuels (états, notifications, compteurs). Il supporte plusieurs variantes (success, warning, danger) et peut être combiné avec d'autres composants comme les boutons ou les liens.

Propriétés

PropriétéTypeDescriptionValeur par défaut
textStringTexte du badge""
variantStringVariante du badge (success, warning, danger, info)"info"
iconStringIcône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*)null
maxNumberValeur maximale pour les badges de compteur (ex. : 99+)null

Exemple d'utilisation

// Badge de succès
const successBadge = new frappe.ui.Badge({
    parent: cur_frm.fields_dict["status"].$wrapper,
    text: "Validé",
    variant: "success"
});

// Badge de compteur
const counterBadge = new frappe.ui.Badge({
    parent: cur_frm.fields_dict["notifications"].$wrapper,
    text: "5",
    variant: "danger",
    max: 99
});

Bonnes pratiques

  • Variantes : Utilisez success pour les états positifs, warning pour les avertissements, danger pour les erreurs, et info pour les informations neutres.
  • Compteurs : Limitez les valeurs élevées avec max pour éviter de déformer le badge (ex. : 99+).
  • Combinaison : Associez les badges à d'autres composants pour renforcer leur visibilité (ex. : badge sur un bouton).

Toast (Notification)

Le composant Toast permet d'afficher des notifications temporaires en haut à droite de l'écran. Il est utilisé pour confirmer des actions, afficher des erreurs ou des informations contextuelles.

Propriétés

PropriétéTypeDescriptionValeur par défaut
messageStringTexte de la notification""
variantStringVariante de la notification (success, warning, danger, info)"info"
durationNumberDurée d'affichage en millisecondes (0 pour infini)3000
actionObjectBouton d'action secondaire (ex. : { label: "Annuler", onClick: () => {} })null

Exemple d'utilisation

// Notification de succès
frappe.show_alert({
    message: "Document enregistré avec succès",
    indicator: "green"
});

// Notification personnalisée avec action
const toast = new frappe.ui.Toast({
    message: "Voulez-vous annuler cette action ?",
    variant: "warning",
    duration: 5000,
    action: {
        label: "Annuler",
        onClick: () => {
            toast.hide();
            frappe.call({
                method: "dokos.api.cancel_action"
            });
        }
    }
});

Bonnes pratiques

  • Durée : Limitez la durée d'affichage à 3-5 secondes pour les notifications standard. Utilisez une durée plus longue ou infinie pour les messages critiques.
  • Actions : Ajoutez un bouton d'action uniquement si l'utilisateur doit prendre une décision immédiate.
  • Variantes : Utilisez success pour les confirmations, warning pour les avertissements, danger pour les erreurs, et info pour les informations neutres.

Chargement asynchrone des options

Les composants Espresso supportent nativement le chargement asynchrone des options, ce qui permet d'optimiser les performances et de proposer des interfaces plus dynamiques. Cette fonctionnalité est particulièrement utile pour :

  • Les menus déroulants avec un grand nombre d'options.
  • Les menus contextuels dont les éléments dépendent du contexte.
  • Les listes de valeurs dynamiques (ex. : projets actifs, utilisateurs connectés).

Implémentation

Pour activer le chargement asynchrone, utilisez les propriétés asyncOptions (pour Dropdown) ou asyncItems (pour ContextMenu). Ces propriétés attendent une fonction asynchrone qui retourne une promesse résolue avec les données.

Exemple avec Dropdown

const dropdown = new frappe.ui.Dropdown({
    parent: cur_frm.fields_dict["project"].$wrapper,
    asyncOptions: async () => {
        const projects = await frappe.call({
            method: "dokos.api.get_active_projects",
            args: { user: frappe.session.user }
        });
        return projects.message.map(project => ({
            label: project.project_name,
            value: project.name
        }));
    },
    placeholder: "Chargement des projets…"
});

Exemple avec ContextMenu

const contextMenu = new frappe.ui.ContextMenu({
    target: cur_frm.fields_dict["items"].grid.wrapper,
    asyncItems: async () => {
        const actions = await frappe.call({
            method: "dokos.api.get_contextual_actions",
            args: {
                doctype: cur_frm.doctype,
                docname: cur_frm.docname
            }
        });
        return actions.message.map(action => ({
            label: action.label,
            action: () => frappe.call({
                method: action.method,
                args: action.args
            })
        }));
    }
});

Gestion des états

Les composants Espresso gèrent automatiquement les états de chargement et les erreurs lors du chargement asynchrone. Voici comment personnaliser ces comportements :

1. État de chargement

Un indicateur de chargement est affiché pendant que les options sont récupérées. Vous pouvez personnaliser le texte affiché avec la propriété placeholder (pour Dropdown) ou en surchargeant le template du composant.

const dropdown = new frappe.ui.Dropdown({
    parent: cur_frm.fields_dict["custom_field"].$wrapper,
    asyncOptions: getAsyncOptions,
    placeholder: "Chargement en cours…"
});

2. Gestion des erreurs

Si la fonction asyncOptions ou asyncItems échoue, un message d'erreur est affiché dans le composant. Vous pouvez intercepter les erreurs pour fournir un retour utilisateur personnalisé :

async function getAsyncOptions() {
    try {
        const options = await frappe.call({
            method: "dokos.api.get_dynamic_options"
        });
        return options.message;
    } catch (e) {
        frappe.show_alert({
            message: __("Impossible de charger les options"),
            indicator: "red"
        });
        return []; // Retourne un tableau vide pour éviter de bloquer l'interface
    }
}

Bonnes pratiques

  • Optimisez les requêtes : Limitez le nombre de requêtes en regroupant les données nécessaires.
  • Cachez les résultats : Si les options ne changent pas fréquemment, envisagez de les mettre en cache côté client pour éviter des requêtes répétées.
  • Gérez les erreurs : Prévoyez un retour utilisateur clair en cas d'échec du chargement.
  • Testez les performances : Vérifiez que le chargement asynchrone améliore effectivement l'expérience utilisateur, notamment sur des connexions lentes.
  • Préchargez les données critiques : Si certaines options sont fréquemment utilisées, préchargez-les pour éviter un délai de chargement.

Personnalisation avancée

Surcharger les styles

Les composants Espresso utilisent des variables CSS pour définir leur apparence. Vous pouvez surcharger ces variables pour personnaliser les styles sans modifier le code source des composants.

Exemple : Modifier la palette de couleurs

/* Dans un fichier CSS personnalisé */
:root {
    --espresso-primary: #4f46e5; /* Bleu indigo */
    --espresso-success: #10b981; /* Vert émeraude */
    --espresso-danger: #ef4444; /* Rouge vif */
}

Exemple : Personnaliser un composant spécifique

/* Cibler uniquement les boutons primaires */
.espresso-button-primary {
    border-radius: 0.5rem;
    font-weight: 600;
}

Créer des composants personnalisés

Vous pouvez étendre les composants Espresso pour créer vos propres composants réutilisables. Voici un exemple de création d'un composant Toggle :

class Toggle extends frappe.ui.Component {
    constructor(opts) {
        super(opts);
        this.state = opts.state || false;
        this.onChange = opts.onChange || (() => {});
        this.render();
    }

    render() {
        this.$wrapper.empty();
        this.$wrapper.addClass("espresso-toggle");
        
        this.$input = $(`<input type="checkbox" ${this.state ? "checked" : ""}>`);
        this.$input.on("change", () => {
            this.state = this.$input.is(":checked");
            this.onChange(this.state);
        });
        
        this.$wrapper.append(this.$input);
    }

    setState(state) {
        this.state = state;
        this.$input.prop("checked", state);
    }
}

// Utilisation
const toggle = new Toggle({
    parent: cur_frm.fields_dict["custom_field"].$wrapper,
    state: true,
    onChange: (state) => {
        frappe.call({
            method: "dokos.api.update_setting",
            args: { setting: "custom_toggle", value: state }
        });
    }
});

Intégration avec les scripts client

Les composants Espresso peuvent être intégrés dans les scripts client de Dokos pour enrichir l'interface des formulaires. Voici un exemple d'intégration dans un script client de Sales Order :

frappe.ui.form.on("Sales Order", {
    refresh(frm) {
        // Ajouter un bouton personnalisé
        frm.add_custom_button(__("Envoyer un rappel"), () => {
            const toast = new frappe.ui.Toast({
                message: __("Rappel envoyé au client"),
                variant: "success"
            });
        }, __("Actions"));
        
        // Ajouter un menu déroulant asynchrone
        const dropdown = new frappe.ui.Dropdown({
            parent: frm.fields_dict["actions"].$wrapper,
            asyncOptions: async () => {
                const options = await frappe.call({
                    method: "dokos.api.get_reminder_options",
                    args: { customer: frm.doc.customer }
                });
                return options.message;
            },
            placeholder: __("Charger les options…")
        });
        
        // Ajouter un badge d'état
        if (frm.doc.status === "Overdue") {
            const badge = new frappe.ui.Badge({
                parent: frm.fields_dict["status"].$wrapper,
                text: __("En retard"),
                variant: "danger"
            });
        }
    }
});

Dépannage

Problèmes courants

1. Les options asynchrones ne s'affichent pas

  • Cause : La fonction asyncOptions ou asyncItems retourne une promesse rejetée ou un tableau vide.
  • Solution : Vérifiez que la fonction retourne bien un tableau d'options valide. Ajoutez une gestion des erreurs pour identifier les problèmes.
async function getAsyncOptions() {
    try {
        const options = await frappe.call({
            method: "dokos.api.get_dynamic_options"
        });
        if (!options.message || !options.message.length) {
            console.warn("Aucune option retournée par l'API");
            return [];
        }
        return options.message;
    } catch (e) {
        console.error("Erreur lors du chargement des options:", e);
        return [];
    }
}

2. Le menu déroulant est vide

  • Cause : Les options sont mal formatées (ex. : absence de propriétés label ou value).
  • Solution : Vérifiez que chaque option a les propriétés attendues par le composant.
// Format attendu pour les options
const options = [
    { label: "Option 1", value: "1" },
    { label: "Option 2", value: "2" }
];

3. Le composant ne s'affiche pas

  • Cause : Le parent (parent) n'est pas un élément DOM valide ou le composant n'est pas correctement initialisé.
  • Solution : Vérifiez que le parent existe et que le composant est bien instancié.
// Vérifiez que le parent existe
if (cur_frm.fields_dict["custom_field"].$wrapper) {
    const dropdown = new frappe.ui.Dropdown({
        parent: cur_frm.fields_dict["custom_field"].$wrapper,
        options: ["Option 1", "Option 2"]
    });
}

Outils de débogage

  • Console du navigateur : Utilisez console.log pour vérifier les données retournées par les fonctions asynchrones.
  • Inspecteur DOM : Vérifiez que les composants sont correctement ajoutés au DOM.
  • Réseau : Dans les outils de développement, vérifiez les requêtes API pour identifier les erreurs ou les temps de réponse lents.

Ressources supplémentaires

Pour toute question ou problème, consultez le forum Dokos ou ouvrez une issue sur GitLab.