Thème
Gestion des Actions (CrudAction)
Les actions s'affichent dans les catégories global (haut de page), item (fin de ligne) ou bulk (actions groupées).
typescript
export interface CrudAction<T = Record<string, any>> {
name: string; // Identifiant unique de l'action
label: string; // Libellé affiché sur le bouton
title?: string; // Titre de la modale/page (supporte {name}, {id}, ex: "Fiche de {name}")
icon?: string; // Classe d'icône FontAwesome (ex: 'fas fa-eye')
style?: string; // Style Bootstrap ('primary', 'secondary', 'danger', 'success', 'warning', 'info')
confirm?: boolean; // Exige une confirmation utilisateur avant exécution
confirmMessage?: string; // Message personnalisé de la modale de confirmation
trackInUrl?: boolean; // Met à jour l'URL avec ?action={name}&id={id} lors de l'ouverture
route?: string; // Avec trackInUrl : gabarit d'URL identifiant l'action (ex: '/users/{id}/edit'), voir « Actions à Route »
displayMode?: 'modal' | 'page' | 'none'; // Mode d'ouverture du composant associé
size?: 'sm' | 'md' | 'lg' | 'xl'; // Taille de la modale si displayMode='modal'
locations?: string[] | any; // Emplacements d'affichage ciblés (ex: ['list'], ['view'], [] pour masqué)
show?: (item: T, context: ActionContext<T>) => boolean; // Condition d'affichage dynamique
component?: Component; // Composant Vue à afficher en modale ou pleine page
componentProps?: Record<string, any>; // Props transmises au composant
subtitle?: string | ((item: T) => string); // Sous-titre en mode pseudo-page
onSuccessAction?: string; // Nom de l'action vers laquelle rediriger après succès (ex: 'view')
handler?: (context: CrudActionHandlerContext<T>) => void | Promise<void>; // { item?, ids?, formData?, filters?, sort?, dir? }
includeFilters?: boolean; // Transmet filtres + tri de la liste (query string en JSON, prop `listQuery` pour les composants)
}Actions à Route (route)
Avec trackInUrl: true, une action peut être identifiée par le chemin de l'URL plutôt que par ?action=…&id=…. route est un gabarit commençant par / :
typescript
{ name: 'edit', trackInUrl: true, displayMode: 'page', route: '/users/{id}/edit' }
{ name: 'review', trackInUrl: true, displayMode: 'page', route: '/users?mode=review&uid={id}' }- Placeholders
{nom}(valeurs de l'item, encodées dans l'URL) dans le chemin et dans la query. Au rechargement, la valeur du placeholderidKey(sinonid) devient l'id de l'action ; sans l'un ni l'autre (/users/{slug}), l'id n'est connu que pour une action ouverte depuis la liste. - Ouverture : navigation vers le chemin interpolé (
/users/5/edit) ; les autres paramètres de la query (page, tri, filtres…) sont conservés,actionetidne sont pas ajoutés. La query du gabarit est fusionnée (?mode=review&uid=5). - Rechargement / lien profond / Précédent : l'action est restaurée si le chemin et la query correspondent au gabarit (la première action qui correspond l'emporte). Le chemin l'emporte sur un éventuel
?action=. - Fermeture : retour à
options.baseRoute; à défaut, au chemin courant si le gabarit a un chemin statique et une query (/users?mode=review…), sinon au dernier chemin de liste visité pendant la session de la page, sinon/. Pour un lien profond, renseignezoptions.baseRoute: la librairie ne peut pas deviner l'URL de votre liste. Les clés de query propres au gabarit (mode,uid) sont retirées. - Ctrl/Cmd+clic : le
hrefgénéré est celui que produit le clic simple. - Sécurité : après interpolation, un chemin doit commencer par un seul
/(ni//hôte, ni/\hôte, ni schéma, ni espace ou caractère de contrôle). Uneroutenon conforme est ignorée : l'action est alors traitée comme une action sansroute(?action=…&id=…sur le chemin courant) et un avertissement est écrit dans la console. UnebaseRoutenon conforme est simplement sautée (repli suivant), sans avertissement. Limites (correspondance en temps linéaire) : un seul placeholder par segment de chemin et par valeur de query (/posts/{year}-{slug}est refusé), pas de fragment (#). options.updateUrl: un adaptateur manuel ne reçoit que la query ; le changement de chemin d'une action àrouten'est alors pas appliqué (à gérer dans l'adaptateur).- Avec
vue-router(détecté automatiquement viaapp.config.globalProperties.$router), la navigation passe parrouter.pushet les liens parrouter.resolve; déclarez une route affichant la page qui contient le<CrudPage>pour chaque chemin de gabarit.
Emplacements d'Affichage (locations)
La propriété locations contrôle précisément dans quels contextes un bouton d'action s'affiche automatiquement :
locations: ['list']: Visible uniquement dans le tableau principal.locations: ['view']: Visible uniquement dans la barre d'outils du header de la ficheview.locations: ['view', 'edit']: Visible dans les barres d'outils des pagesviewetedit, mais pas dans le tableau.locations: []: Masqué de toutes les barres d'outils automatiques. L'action reste enregistrée pour être déclenchée sur mesure via slot ou par code (triggerAction).undefined/ non spécifié /null/true: Interprété comme['*'](visible partout où c'est approprié).
Filtrage dynamique via show(item, context)
La fonction show reçoit le contexte de l'emplacement courant :
typescript
show: (item: User, context: { location: string; activeAction?: CrudAction }) => {
// context.location vaut 'list' sur le tableau, ou le nom de l'action ouverte ('view', 'edit'...)
if (context.location === 'list') {
return item.status === 'draft';
}
return true;
}Mode d'Affichage du Bouton (buttonDisplay)
Par défaut, si une action possède une icône et un label, elle affiche les deux (both). Vous pouvez personnaliser l'affichage via la propriété buttonDisplay (ou button_display en JSON) :
'both'(défaut) : Affiche l'icône ET le libellé si les deux sont définis (ou seulement celui disponible).'icon': Affiche uniquement l'icône. Le label est automatiquement placé en attributtitle(infobulle au survol) etaria-labelpour l'accessibilité. Si aucune icône n'est fournie, bascule automatiquement sur le label pour éviter un bouton vide. Une action sanslabelnititlereçoit sonnamecommearia-label: définisseztitlepour un nom lisible (ex.{ name: 'delete', label: '', title: 'Supprimer', icon: 'fas fa-trash' }).'text': Affiche uniquement le label (même si une icône est configurée).'auto': Équivalent à'both'(affiche les deux si disponibles).
Configuration contextuelle par Emplacement
Vous pouvez spécifier un objet associant un mode à chaque emplacement :
typescript
{
name: 'edit',
label: 'Modifier',
icon: 'fas fa-pencil',
locations: ['list', 'view', 'dropdown-more'],
buttonDisplay: {
list: 'icon', // Compact (icône seule + tooltip) dans le tableau
view: 'both', // Icône + texte dans la fiche détaillée
'dropdown-*': 'both', // Icône + texte dans les menus déroulants
default: 'both' // Valeur de repli
}
}Redirection après Succès (onSuccessAction)
Pour les actions de type form (ou les actions personnalisées émettant l'événement success), vous pouvez configurer une redirection automatique une fois l'opération terminée.
typescript
{
name: 'create',
label: 'Créer',
type: 'form',
onSuccessAction: 'view-page' // Ouvre l'action "view-page" avec les données du nouvel élément
}Options de Rafraîchissement Post-Succès (onSuccess*)
Sur les actions (en particulier form et confirm), vous pouvez piloter finement le comportement de rafraîchissement après exécution réussie :
| Option | Type | Défaut | Description |
|---|---|---|---|
onSuccessRefreshList | boolean | false | Si true, force le rechargement immédiat de la liste via l'API. |
onSuccessInvalidateList | boolean | true | Si true, invalide le cache de la liste (invalidateList()). |
onSuccessRefreshCurrentPage | boolean | true | Si true, déclenche automatiquement le rafraîchissement de la page active. |
Transmettre les Filtres et le Tri de la Liste (includeFilters)
Une action globale ou bulk (ex : un export) a souvent besoin de connaître l'état courant de la liste pour ne traiter que les résultats filtrés.
Handlers : le contexte reçu contient toujours filters, sort et dir, figés au moment du clic (avant la confirmation éventuelle). Aucune option n'est requise.
typescript
{
name: 'export',
label: 'Exporter',
icon: 'fas fa-file-export',
handler: async ({ filters, sort, dir }) => {
await api.export({ filters, sort, dir });
}
}Composants d'action (modale / page) : avec includeFilters: true, le composant reçoit une prop listQuery: CrudListQuery ({ filters, sort?, dir? }). CrudRemoteForm et CrudRemoteView l'ajoutent automatiquement à leurs URLs (fetchUrl, submitUrl). Sans l'option, la prop n'est pas transmise. Contrairement au contexte des handlers, listQuery est réactive : elle reflète l'état courant de la liste.
Format (identique à la requête de liste) : ?sort=name&dir=asc&filters[status]=published&filters[tags][]=a. Les helpers buildListQueryParams, appendQueryParams et appendListQuery sont exportés pour construire vos propres URLs.
Édition en Ligne (inline-edit)
Permet de modifier directement les champs d'une ligne du tableau :
typescript
{
name: 'quick-edit',
label: 'Éditer en ligne',
type: 'inline-edit',
inlineOptions: {
saveLabel: 'Enregistrer',
saveStyle: 'success',
cancelLabel: 'Annuler',
cancelStyle: 'secondary'
},
formFields: {
role: createFormField('role', 'choice', { choices: [{ label: 'Admin', value: 'Admin' }, { label: 'User', value: 'User' }] })
},
handler: async ({ item, formData }) => {
await api.update(item.id, formData);
}
}Créer un Type d'Action Spécifique
Vous pouvez étendre les types d'actions par défaut (form, confirm, inline-edit, view) en définissant vos propres gestionnaires ou composants d'action.
1. Action sous forme de composant (ex: CrudRemoteView personnalisé)
Si vous souhaitez afficher une page ou une modale complexe, utilisez la propriété component de l'action. Le composant recevra par défaut en props l'objet item cliqué.
typescript
import MyCustomView from '@/components/MyCustomView.vue';
{
name: 'advanced-view',
label: 'Vue Avancée',
displayMode: 'page', // ou 'modal'
component: MyCustomView,
componentProps: { extraOption: true } // Props supplémentaires passées au composant
}Dans MyCustomView.vue, déclarez la prop item pour récupérer l'élément :
vue
<script setup lang="ts">
defineProps<{ item: any, extraOption: boolean }>();
</script>
<template>
<div>
<h3>Détails pour {{ item.name }}</h3>
<!-- Votre vue personnalisée -->
</div>
</template>2. Action exécutant une fonction (similaire à confirm)
Pour exécuter une logique métier sans interface lourde, utilisez la propriété handler. Vous pouvez combiner cela avec confirm: true pour demander confirmation avant l'exécution.
typescript
{
name: 'archive',
label: 'Archiver',
style: 'warning',
confirm: true,
confirmMessage: 'Voulez-vous vraiment archiver cet élément ?',
onSuccessRefreshList: true, // Rafraîchit automatiquement la liste en cas de succès
handler: async ({ item }) => {
// Logique personnalisée (ex: appel API)
await api.post(`/items/${item.id}/archive`);
}
}Boutons Dropdown (type: 'dropdown')
Vous pouvez regrouper plusieurs actions sous un menu déroulant (dropdown) dans le tableau principal, la barre d'outils globale, ou la barre d'outils du header de page.
Configuration d'un bouton Dropdown
Déclarez une action avec type: 'dropdown' :
typescript
actions: {
item: [
{
name: 'more', // Sert de clé de ciblage "dropdown-more"
type: 'dropdown',
label: 'Actions',
icon: 'fas fa-ellipsis-v',
style: 'outline-secondary',
locations: ['list'], // Emplacement où s'affiche le bouton déclencheur
dropdownAlign: 'right', // 'right' (défaut en table) ou 'left'
autoHideIfEmpty: true // Masque le dropdown s'il ne contient aucune action visible (défaut: true)
},
// Actions enfants qui s'affichent dans ce dropdown
{
name: 'duplicate',
label: 'Dupliquer',
icon: 'fas fa-copy',
locations: ['dropdown-more'], // Cible le dropdown "more"
handler: ({ item }) => { ... }
},
{
name: 'archive',
label: 'Archiver',
icon: 'fas fa-box-archive',
style: 'danger',
confirm: true,
locations: ['dropdown-more'],
handler: ({ item }) => { ... }
}
]
}Règles de ciblage
locations: ['dropdown-{name}']: L'action apparaîtra dans le menu déroulant portant lenamecorrespondant (oukey).- Multi-emplacements : Une action peut avoir
locations: ['dropdown-more', 'view']: elle sera dans le dropdown sur la liste, et sous forme de bouton classique dans le header de la ficheview. - Protection Wildcard : Une action standard avec
locations: ['*'](ou sanslocations) n'apparaît jamais par erreur dans un dropdown. Pour cibler tous les dropdowns, utilisez explicitementlocations: ['dropdown-*']. - Ordre explicite (
actions) : Un dropdown peut optionnellement définiractions: ['duplicate', 'archive']pour forcer un ordre précis au sein du menu sans dépendre de l'ordre global. - Masquage automatique (
autoHideIfEmpty) : Si toutes les sous-actions sont masquées (viashow(item)ou les permissionsallowedActionsKey), le bouton dropdown se masque automatiquement pour éviter d'afficher un menu vide.