Skip to content

Déclarer une ressource ​

Le problème ​

Une page de liste décrit trois fois le même objet : une fois en colonnes pour le tableau, une fois en champs pour le formulaire, une fois en lignes pour la vue de détail. Plus la validation à côté, les clés de recherche, la règle de droits par opération.

Ces descriptions divergent. Toujours dans le même sens, d'ailleurs : c'est le formulaire qui oublie le champ que le tableau affiche, et la validation qui ignore le champ ajouté la semaine dernière. Rien ne le signale — l'écran se rend, les colonnes sont là, et le champ manquant ne se voit qu'au moment où quelqu'un essaie de le saisir.

La déclaration ​

defineResource décrit l'objet une fois. Tout le reste en est dérivé.

tsx
import { defineResource, CrudPage } from "@sia-ui/react-web";

const FACTURE = defineResource<Facture>({
  name: "facture",
  label: { singular: "Facture", plural: "Factures", create: "Nouvelle facture" },
  key: "id",
  permissions: "auto",
  formColumns: 2,
  fields: {
    reference: { label: "Référence", required: true },
    client:    { required: true, truncate: true },
    email:     { type: "email", label: "Courriel", colSpan: 2 },
    montant:   { type: "currency", currency: "XOF", required: true },
    echeance:  { type: "date", label: "Échéance" },
    statut:    {
      type: "select",
      options: [
        { value: "payee", label: "Payée" },
        { value: "attente", label: "En attente" },
      ],
      tones: { payee: "success", attente: "warning" },
      defaultValue: "attente",
    },
    notes:  { type: "textarea", colSpan: 2 },
    creeLe: { type: "date", label: "Créée le", readOnly: true },
  },
});

La page entière tient alors en cinq props :

tsx
<CrudPage
  resource={FACTURE}
  data={factures}
  can={can}
  onSubmit={(valeurs, { mode, row }) => api.enregistrer(mode, row, valeurs)}
  onDelete={(f) => api.supprimer(f.id)}
/>

Ce qu'on obtient : le titre, le bouton de création, les colonnes formatées, les trois boîtes de dialogue, la validation, la recherche locale, la clé de ligne et les quatre règles de droits.

Ce qui est dérivé de quoi ​

Chaque dérivation cède à la prop correspondante. Passer columns ne coûte ni le formulaire, ni les droits :

Prop fournieRemplace
columnsles colonnes dérivées
fieldsles champs du formulaire
validatela validation dérivée
searchKeysles clés de recherche
getRowKeyla clé de ligne
title, descriptionle libellé de la ressource
operations.*.permissionle droit dérivé du nom

Où une propriété apparaît ​

Trois surfaces, trois défauts, chacun réglable :

RéglagePar défaut
inTablevrai, sauf textarea, rich-text, markdown, json, password, file, image, otp, hidden
inFormvrai, sauf readOnly et value ; "create" ou "edit" pour un seul des deux formulaires
inDetailvrai
searchablevrai pour text, textarea, email, phone, autocomplete, reference

Le détail n'est pas le tableau. Une note de trois phrases n'a pas sa place dans une cellule, mais c'est précisément au détail qu'on va la lire : elle sort du tableau et reste dans la boîte de consultation.

readOnly marque ce qui se lit sans se saisir — un identifiant, une date de création, un total calculé. Sans lui, une page de modification renverrait au serveur des champs qu'il calcule lui-même.

Création et modification ​

Un mot de passe initial se saisit à la création, jamais à la modification. inForm et required acceptent un mode :

ts
motDePasse: { type: "password", inForm: "create", required: "create" },
telephone: { type: "phone", required: "edit" }, // exigé une fois le compte créé

CrudPage dérive deux formulaires — champs, validation — et ouvre le bon. resourceFormFields(resource, mode) et resourceValidator(resource, mode) prennent le mode ; "create" par défaut. Des fields passés à la main servent aux deux.

Régler le contrôle d'une propriété ​

controlProps passe tel quel au contrôle de saisie — un Select cherchable, une longueur maximale — sans redéclarer les champs dans la page :

ts
type: {
  type: "select",
  options: typesDeFournisseur,
  controlProps: { searchable: true },
},

C'est le controlProps de Field, typé par les props de tous ses contrôles.

Les colonnes calculées ​

Un nom complet, un solde : une colonne qui n'est pas une propriété de la ligne. value la calcule, sans élargir le type T :

ts
nomComplet: { label: "Nom", value: (compte) => `${compte.prenom} ${compte.nom}` },

Le rendu par défaut, la carte et la recherche locale passent par elle (accessor de la colonne). Une valeur calculée ne se saisit pas : elle sort du formulaire et des valeurs de départ, sauf inForm explicite.

Le rendu déduit du type ​

Une propriété n'a pas besoin d'une fonction de rendu pour s'afficher correctement :

TypeRendu
currencyAmountDisplay, aligné à droite, dans la devise déclarée
number, sliderformatNumber, aligné à droite
date, datetimeformatDate / formatDateTime
select, radiole libellé de l'option; une Badge si tones est déclaré
checkbox, switch« Oui » / « Non »
multiselect, tagsles libellés, séparés par des virgules

render reprend la main sur n'importe lequel, pour cette propriété seule.

La validation ​

Elle sort des types et des obligations, sans qu'on écrive de schéma :

  • required: true ajoute la règle correspondante ;
  • type: "email" valide l'adresse, type: "phone" le numéro ;
  • rules ajoute les siennes — elles ne remplacent pas les précédentes.
tsx
montant: { type: "currency", required: true, rules: [min(1000)] }

Aucune bibliothèque n'est imposée. La validation dérivée est un FormValidator, le même contrat que Form : un schéma Zod se branche à sa place en passant validate.

Les droits ​

permissions: "auto" applique la convention <nom>.<verbe> :

OpérationRègle
créationfacture.creer
consultationfacture.lire
modificationfacture.modifier
suppressionfacture.supprimer

Chaque règle se déclare aussi à la main, et operations.*.permission passe devant. Comme partout ailleurs, ceci masque un bouton; ça ne protège pas l'API.

Ce qui reste à écrire ​

La déclaration ne remplace pas le métier. Trois choses restent à fournir :

  • onSubmit — ce qu'on fait d'un formulaire envoyé ;
  • onDelete — ce qu'on fait d'une suppression confirmée ;
  • data — les lignes, et leur chargement.

Et tout ce qui est propre à un écran s'ajoute par-dessus sans toucher à la déclaration : extraRowActions pour une relance, operations.delete.hidden pour une facture déjà payée, table pour la densité ou le mode cartes.

Voir aussi ​

Publié sous licence MIT.