Skip to content

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 placeholder idKey (sinon id) 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, action et id ne 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, renseignez options.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 href gé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). Une route non conforme est ignorée : l'action est alors traitée comme une action sans route (?action=…&id=… sur le chemin courant) et un avertissement est écrit dans la console. Une baseRoute non 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 à route n'est alors pas appliqué (à gérer dans l'adaptateur).
  • Avec vue-router (détecté automatiquement via app.config.globalProperties.$router), la navigation passe par router.push et les liens par router.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 fiche view.
  • locations: ['view', 'edit'] : Visible dans les barres d'outils des pages view et edit, 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 attribut title (infobulle au survol) et aria-label pour l'accessibilité. Si aucune icône n'est fournie, bascule automatiquement sur le label pour éviter un bouton vide. Une action sans label ni title reçoit son name comme aria-label : définissez title pour 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 :

OptionTypeDéfautDescription
onSuccessRefreshListbooleanfalseSi true, force le rechargement immédiat de la liste via l'API.
onSuccessInvalidateListbooleantrueSi true, invalide le cache de la liste (invalidateList()).
onSuccessRefreshCurrentPagebooleantrueSi 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 le name correspondant (ou key).
  • 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 fiche view.
  • Protection Wildcard : Une action standard avec locations: ['*'] (ou sans locations) n'apparaît jamais par erreur dans un dropdown. Pour cibler tous les dropdowns, utilisez explicitement locations: ['dropdown-*'].
  • Ordre explicite (actions) : Un dropdown peut optionnellement définir actions: ['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 (via show(item) ou les permissions allowedActionsKey), le bouton dropdown se masque automatiquement pour éviter d'afficher un menu vide.