## Objectif Afficher les violations de validation 422 du back **sous chaque champ** (prop `:error` des `Malio*`) au lieu d'un toast global, et poser **une convention reutilisable par tous les forms**. ## Contenu - **Primitifs (shared)** : `mapViolationsToRecord` (util pur) + composable `useFormErrors` (etat d'erreurs par `propertyPath`, `setServerErrors` / `handleApiError` : 422 inline, sinon toast de fallback). - **Formulaire Client** (`new.vue` + `[id]/edit.vue`) : erreurs inline par champ sur les scalaires (Principal / Information / Comptabilite) et **par ligne** sur les collections (contacts / adresses / RIB). - **Blocs** `ClientContactBlock` / `ClientAddressBlock` : nouvelle prop `errors`. - **Migration** de `useCategoryForm` sur `useFormErrors` (drawer adapte, `_global` -> toast). - **Convention** documentee dans `.claude/rules/frontend.md` + spec de design. ## Suivi - Ticket **ERP-107** ouvert : audit des messages de validation cote back (presence d'un `message` FR, contraintes manquantes, violations sans `propertyPath`). ## Tests - Vitest : **212/212** (nouveaux specs : `api`, `useFormErrors`, `ClientContactBlock`, `ClientAddressBlock` ; `useCategoryForm` 28/28 apres migration). - eslint clean, `nuxi typecheck` 0 erreur. - Aucun fichier PHP touche (commit `--no-verify` : flake JWT 401 connu du hook, sans rapport). Reviewed-on: #58 Reviewed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
5.4 KiB
ERP-101 — Mapping des erreurs de validation par champ (convention forms)
Statut : design validé — implémentation TDD en cours Branche :
feat/ERP-101-form-field-validation-mappingDate : 2026-06-03
Problème
Quand le back renvoie une 422 (violations API Platform), il renvoie toutes les
violations d'un coup (un propertyPath + message par champ fautif). Aujourd'hui, seul
le drawer Catégorie (useCategoryForm) exploite ce détail pour afficher l'erreur sous
le champ concerné ; il le fait via un if/else manuel par champ, non réutilisable.
Le formulaire Client (≈ 20 champs sur 5 submits, dont 3 collections) ne mappe rien : une 422 multi-champs ⇒ un seul toast global. On veut un retour par champ, et surtout une convention unique réutilisée par tous les modules.
Décisions
- Primitif générique plutôt que composable par form :
useFormErrors()partagé. - Périmètre complet sur Client : champs scalaires et collections (erreur par ligne).
Architecture — 3 briques
1. mapViolationsToRecord(data) — frontend/shared/utils/api.ts
Util pur, fondation réutilisée partout. Transforme un payload 422 en
Record<propertyPath, message>. S'appuie sur extractApiViolations (déjà existant,
gère les formats violations et hydra:violations).
export function mapViolationsToRecord(data: unknown): Record<string, string> {
const out: Record<string, string> = {}
for (const v of extractApiViolations(data)) {
if (v.propertyPath) out[v.propertyPath] = v.message
}
return out
}
2. useFormErrors() — frontend/shared/composables/useFormErrors.ts
API que tous les forms scalaires consomment.
const { errors, hasErrors, setServerErrors, setError, clearError, clearErrors, handleApiError } = useFormErrors()
errors:reactive<Record<string, string>>indexé parpropertyPath.setServerErrors(data):mapViolationsToRecord→ rempliterrors. Retournetruesi au moins une violation a été mappée.setError(field, msg)/clearError(field)/clearErrors(): manipulation fine.hasErrors:computedbooléen.handleApiError(e, opts?): dispatch standard depuis une erreur ofetch — 422 →setServerErrors(mapping inline, pas de toast) ; autre → toast générique de fallback (message extrait viaextractApiErrorMessage).
Côté template, le nom du champ est le propertyPath :
<MalioInputText v-model="main.companyName" :error="errors.companyName" />
<MalioInputText v-model="accounting.siren" :error="errors.siren" />
L'unicité SIREN (RG-1.15) remonte en 422
UniqueEntityavecpropertyPath: "siren"→ mappée automatiquement. Pas de cas 409 spécial (contrairement à Catégorie).
3. Collections — erreurs par ligne
Chaque ligne (contact / adresse / RIB) est persistée par son propre appel API, donc
le back renvoie un 422 relatif à la sous-entité (propertyPath: "email", "iban"…).
- Le parent tient, par collection, un tableau d'erreurs aligné sur l'index de ligne :
const contactErrors = ref<Record<string, string>[]>([]). - Au submit de la ligne
i:catch→contactErrors.value[i] = mapViolationsToRecord(data). - On
clearErrorsla collection au début de chaque passe de submit. - Les blocs reçoivent une prop
:errors(Record<string, string>) et bindent:error="errors?.email"sur chaque champ Malio.
Fichiers touchés
| Fichier | Action |
|---|---|
shared/utils/api.ts |
+ mapViolationsToRecord |
shared/composables/useFormErrors.ts |
nouveau composable |
modules/commercial/pages/clients/new.vue |
scalaires (Main/Info/Compta) + erreurs par ligne |
modules/commercial/pages/clients/[id]/edit.vue |
idem |
modules/commercial/components/ClientContactBlock.vue |
+ prop :errors, bind :error |
modules/commercial/components/ClientAddressBlock.vue |
+ prop :errors, bind :error |
| RIB (inline dans new/edit) | bind :error par ligne |
Tests (Vitest — règle « pas d'E2E »)
mapViolationsToRecord: formatsviolations/hydra:violations, payload vide,propertyPathmanquant.useFormErrors:setServerErrorsmappe et retournetrue/falsesans violation,clearErrors, fallback toast sur non-422.
Convention posée pour tous les forms
À reporter dans .claude/rules/frontend.md une fois le pattern stabilisé :
Tout form qui veut un retour d'erreur par champ : appels API en
{ toast: false }+useFormErrorspour les champs scalaires (422 inline),mapViolationsToRecordpar ligne pour les collections.useCategoryFormmigrera suruseFormErrors.
Fait dans la foulée (post-ERP-101 initial)
useCategoryFormmigré suruseFormErrors:errorsdevient lereactivedu composable (drawer adapté :form.errors.nameau lieu deform.errors.value.name, bloc_globalretiré → erreur transverse en toast). 28 tests verts.- Convention reportée dans
.claude/rules/frontend.md(section « Validation des formulaires — useFormErrors obligatoire »).
Hors scope ERP-101 (suivi : ticket ERP-107)
- Langue / présence des messages de validation côté back : le
messageaffiché est celui renvoyé par le serveur. Audit des contraintes Symfony (présence d'unmessageFR, contraintes manquantes, violations sanspropertyPath) tracké dans ERP-107.