Files
Starseed/frontend/shared/utils/api.ts
T
tristan ee3bbea649
Auto Tag Develop / tag (push) Successful in 7s
feat(front) : mapping des erreurs de validation 422 par champ (ERP-101) (#58)
## 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>
2026-06-04 08:41:19 +00:00

112 lines
3.9 KiB
TypeScript

/**
* Schemas Hydra / API Platform 4.
*
* Important : API Platform 4 abandonne le prefixe `hydra:` dans les noms de
* proprietes (compare a la version 3). Un GET /api/audit-logs renvoie :
* { "@context": ..., "@id": ..., "@type": "...",
* "member": [...],
* "totalItems": 30,
* "view": { "@id": ..., "@type": "...", "first": ..., "next": ..., ... } }
*
* En `application/json` (sans ld), API Platform retourne un simple tableau
* plat sans ces metadonnees — on doit donc explicitement demander
* `application/ld+json` (via l'option `headers: { Accept: ... }` de useApi)
* pour avoir acces a la pagination.
*/
export interface HydraView {
'@id'?: string
'@type'?: string
first?: string
last?: string
next?: string
previous?: string
}
export interface HydraCollection<T> {
member: T[]
totalItems: number
view?: HydraView
}
export function extractHydraMembers<T>(collection: HydraCollection<T>): T[] {
return collection.member ?? []
}
/**
* Une violation de contrainte API Platform (reponse 422). Le `propertyPath`
* pointe le champ concerne, `message` est le libelle a afficher.
*/
export interface ApiViolation {
propertyPath: string
message: string
}
/**
* Extrait les violations d'un payload d'erreur 422 d'API Platform 4. Supporte
* les deux formats de negociation (`violations` ou `hydra:violations`) et
* renvoie un tableau vide si le payload n'en contient pas d'exploitables.
*
* Utilise par useCategoryForm et tout futur composable de formulaire qui
* doit mapper les violations serveur sur ses champs.
*/
export function extractApiViolations(data: unknown): ApiViolation[] {
if (!data || typeof data !== 'object') return []
const record = data as Record<string, unknown>
const raw = record.violations ?? record['hydra:violations']
if (!Array.isArray(raw)) return []
const out: ApiViolation[] = []
for (const v of raw) {
if (!v || typeof v !== 'object') continue
const obj = v as Record<string, unknown>
out.push({
propertyPath: String(obj.propertyPath ?? ''),
message: String(obj.message ?? ''),
})
}
return out
}
/**
* Transforme un payload d'erreur 422 d'API Platform en dictionnaire
* `{ propertyPath: message }`, directement consommable par la prop `:error`
* des composants `Malio*` (le nom du champ cote front = le `propertyPath`
* renvoye par le back). Fondation du mapping erreur→champ des formulaires :
* utilise par `useFormErrors` (champs scalaires) et par les boucles de submit
* de collections (erreur par ligne).
*
* Les violations sans `propertyPath` (erreur globale) sont ignorees ; en cas
* de doublon de `propertyPath`, la derniere violation l'emporte.
*/
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
}
/**
* Extrait un message d'erreur lisible depuis un payload Hydra / JSON
* d'erreur API Platform. Essaie les champs courants dans l'ordre :
* `hydra:description` → `detail` → `description` → `message` → `error` →
* `title` → `hydra:title`. Renvoie '' si rien d'exploitable.
*
* Si `data` est une string, la renvoie telle quelle (cas des erreurs
* Symfony en text/plain ou des messages bruts).
*/
export function extractApiErrorMessage(data: unknown): string {
if (typeof data === 'string') return data
if (!data || typeof data !== 'object') return ''
const record = data as Record<string, unknown>
return (
(record['hydra:description'] as string)
?? (record.detail as string)
?? (record.description as string)
?? (record.message as string)
?? (record.error as string)
?? (record.title as string)
?? (record['hydra:title'] as string)
?? ''
)
}