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.
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).
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.
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.
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é | Type | Description | Valeur par défaut |
|---|---|---|---|
options | Array | Liste des options statiques (remplacé par asyncOptions si fourni) | [] |
asyncOptions | Function | Fonction asynchrone retournant une promesse résolue avec les options | null |
placeholder | String | Texte affiché lorsque aucune option n'est sélectionnée | "Sélectionner une option" |
icon | String | Icône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*) | null |
disabled | Boolean | Désactive le menu déroulant | false |
searchable | Boolean | Active la recherche dans les options | false |
// 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…"
});
searchable: true) ou utilisez une pagination côté serveur.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é | Type | Description | Valeur par défaut |
|---|---|---|---|
target | HTMLElement | Élément DOM sur lequel le menu contextuel est attaché | null |
items | Array | Liste des éléments statiques du menu | [] |
asyncItems | Function | Fonction asynchrone retournant une promesse résolue avec les éléments du menu | null |
onClick | Function | Callback appelé lorsqu'un élément est cliqué | null |
// 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;
}
});
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é | Type | Description | Valeur par défaut |
|---|---|---|---|
label | String | Texte du bouton | "Bouton" |
variant | String | Variante du bouton (primary, secondary, danger, ghost) | "secondary" |
icon | String | Icône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*) | null |
disabled | Boolean | Désactive le bouton | false |
onClick | Function | Callback appelé lorsque le bouton est cliqué | null |
loading | Boolean | Affiche un indicateur de chargement | false |
// 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);
}
});
primary pour les actions principales, secondary pour les actions secondaires, et danger pour les actions destructrices.loading pendant les opérations asynchrones pour informer l'utilisateur.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é | Type | Description | Valeur par défaut |
|---|---|---|---|
text | String | Texte du badge | "" |
variant | String | Variante du badge (success, warning, danger, info) | "info" |
icon | String | Icône à afficher à gauche du texte (format i-mdi-* ou i-fluent-*) | null |
max | Number | Valeur maximale pour les badges de compteur (ex. : 99+) | null |
// 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
});
success pour les états positifs, warning pour les avertissements, danger pour les erreurs, et info pour les informations neutres.max pour éviter de déformer le badge (ex. : 99+).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é | Type | Description | Valeur par défaut |
|---|---|---|---|
message | String | Texte de la notification | "" |
variant | String | Variante de la notification (success, warning, danger, info) | "info" |
duration | Number | Durée d'affichage en millisecondes (0 pour infini) | 3000 |
action | Object | Bouton d'action secondaire (ex. : { label: "Annuler", onClick: () => {} }) | null |
// 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"
});
}
}
});
success pour les confirmations, warning pour les avertissements, danger pour les erreurs, et info pour les informations neutres.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 :
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.
Dropdownconst 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…"
});
ContextMenuconst 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
})
}));
}
});
Les composants Espresso gèrent automatiquement les états de chargement et les erreurs lors du chargement asynchrone. Voici comment personnaliser ces comportements :
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…"
});
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
}
}
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.
/* Dans un fichier CSS personnalisé */
:root {
--espresso-primary: #4f46e5; /* Bleu indigo */
--espresso-success: #10b981; /* Vert émeraude */
--espresso-danger: #ef4444; /* Rouge vif */
}
/* Cibler uniquement les boutons primaires */
.espresso-button-primary {
border-radius: 0.5rem;
font-weight: 600;
}
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 }
});
}
});
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"
});
}
}
});
asyncOptions ou asyncItems retourne une promesse rejetée ou un tableau vide.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 [];
}
}
label ou value).// Format attendu pour les options
const options = [
{ label: "Option 1", value: "1" },
{ label: "Option 2", value: "2" }
];
parent) n'est pas un élément DOM valide ou le composant n'est pas correctement initialisé.// 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"]
});
}
console.log pour vérifier les données retournées par les fonctions asynchrones.Pour toute question ou problème, consultez le forum Dokos ou ouvrez une issue sur GitLab.