502d1a216b
- composable useFormErrors + util mapViolationsToRecord (shared) - formulaire Client (new + edit) : erreurs inline par champ (scalaires) et par ligne pour les collections (contacts / adresses / RIB) - blocs ClientContactBlock / ClientAddressBlock : prop errors - migration de useCategoryForm sur useFormErrors - convention documentee dans .claude/rules/frontend.md
120 lines
5.4 KiB
Markdown
120 lines
5.4 KiB
Markdown
# 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-mapping`
|
|
> Date : 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
|
|
|
|
1. **Primitif générique** plutôt que composable par form : `useFormErrors()` partagé.
|
|
2. **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`).
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
const { errors, hasErrors, setServerErrors, setError, clearError, clearErrors, handleApiError } = useFormErrors()
|
|
```
|
|
|
|
- `errors` : `reactive<Record<string, string>>` indexé par `propertyPath`.
|
|
- `setServerErrors(data)` : `mapViolationsToRecord` → remplit `errors`. Retourne `true`
|
|
si au moins une violation a été mappée.
|
|
- `setError(field, msg)` / `clearError(field)` / `clearErrors()` : manipulation fine.
|
|
- `hasErrors` : `computed` boolé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 via `extractApiErrorMessage`).
|
|
|
|
Côté template, le nom du champ **est** le `propertyPath` :
|
|
|
|
```vue
|
|
<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 `UniqueEntity` avec `propertyPath: "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 `clearErrors` la 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` : formats `violations` / `hydra:violations`, payload vide,
|
|
`propertyPath` manquant.
|
|
- `useFormErrors` : `setServerErrors` mappe et retourne `true` / `false` sans 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 }` +
|
|
> `useFormErrors` pour les champs scalaires (422 inline), `mapViolationsToRecord` par
|
|
> ligne pour les collections. `useCategoryForm` migrera sur `useFormErrors`.
|
|
|
|
## Fait dans la foulée (post-ERP-101 initial)
|
|
|
|
- **`useCategoryForm` migré sur `useFormErrors`** : `errors` devient le `reactive` du
|
|
composable (drawer adapté : `form.errors.name` au lieu de `form.errors.value.name`,
|
|
bloc `_global` retiré → 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 `message` affiché est celui
|
|
renvoyé par le serveur. Audit des contraintes Symfony (présence d'un `message` FR,
|
|
contraintes manquantes, violations sans `propertyPath`) tracké dans **ERP-107**.
|