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é.
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 :
<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 fournie | Remplace |
|---|---|
columns | les colonnes dérivées |
fields | les champs du formulaire |
validate | la validation dérivée |
searchKeys | les clés de recherche |
getRowKey | la clé de ligne |
title, description | le libellé de la ressource |
operations.*.permission | le droit dérivé du nom |
Où une propriété apparaît
Trois surfaces, trois défauts, chacun réglable :
| Réglage | Par défaut |
|---|---|
inTable | vrai, sauf textarea, rich-text, markdown, json, password, file, image, otp, hidden |
inForm | vrai, sauf readOnly et value ; "create" ou "edit" pour un seul des deux formulaires |
inDetail | vrai |
searchable | vrai 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 :
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 :
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 :
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 :
| Type | Rendu |
|---|---|
currency | AmountDisplay, aligné à droite, dans la devise déclarée |
number, slider | formatNumber, aligné à droite |
date, datetime | formatDate / formatDateTime |
select, radio | le libellé de l'option; une Badge si tones est déclaré |
checkbox, switch | « Oui » / « Non » |
multiselect, tags | les 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: trueajoute la règle correspondante ;type: "email"valide l'adresse,type: "phone"le numéro ;rulesajoute les siennes — elles ne remplacent pas les précédentes.
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ération | Règle |
|---|---|
| création | facture.creer |
| consultation | facture.lire |
| modification | facture.modifier |
| suppression | facture.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
- La page de liste et son tableau — les quatre opérations, les boîtes, et le tableau sous-jacent.
- L'adaptateur de formulaire — le contrat
FormAdapterauquel la validation dérivée se conforme. - La session et ses droits — la grammaire des règles de permission.