feat(front) : mapping des erreurs de validation 422 par champ (ERP-101)
- 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
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Composable d'erreurs de formulaire — convention de mapping erreur→champ pour
|
||||
* tous les forms du projet (ERP-101).
|
||||
*
|
||||
* Le back renvoie TOUTES les violations d'une 422 d'un coup (un `propertyPath`
|
||||
* + `message` par champ fautif). Ce composable centralise leur affichage
|
||||
* inline : il tient un `Record<propertyPath, message>` reactif que le template
|
||||
* branche directement sur la prop `:error` des composants `Malio*` (le nom du
|
||||
* champ cote front = le `propertyPath` cote back, donc aucun mapping manuel).
|
||||
*
|
||||
* Chaque appel cree son propre etat (refs internes a la fonction) — un form =
|
||||
* une instance, pas de singleton partage.
|
||||
*
|
||||
* Convention d'usage : les appels API qui veulent un retour inline doivent
|
||||
* passer `{ toast: false }` a `useApi` (sinon le toast natif masque le mapping
|
||||
* fin), puis router l'erreur via `handleApiError`. Pour les collections (1
|
||||
* appel par ligne), utiliser directement `mapViolationsToRecord` par ligne.
|
||||
*/
|
||||
import { computed, reactive } from 'vue'
|
||||
import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api'
|
||||
|
||||
/**
|
||||
* Erreur HTTP capturee par ofetch. On n'expose que les champs lus ici (status
|
||||
* + payload) pour eviter de typer toute la lib.
|
||||
*/
|
||||
interface ApiFetchError {
|
||||
response?: {
|
||||
status?: number
|
||||
_data?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/** Options de `handleApiError`. */
|
||||
interface HandleApiErrorOptions {
|
||||
/** Message de toast si l'erreur n'est pas une 422 exploitable. */
|
||||
fallbackMessage?: string
|
||||
}
|
||||
|
||||
export function useFormErrors() {
|
||||
const toast = useToast()
|
||||
|
||||
// Etat d'erreurs indexe par propertyPath. Reactif : muter une cle suffit a
|
||||
// rafraichir la prop `:error` du champ correspondant.
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
const hasErrors = computed(() => Object.keys(errors).length > 0)
|
||||
|
||||
/** Pose une erreur sur un champ. */
|
||||
function setError(field: string, message: string): void {
|
||||
errors[field] = message
|
||||
}
|
||||
|
||||
/** Retire l'erreur d'un champ (no-op si absente). */
|
||||
function clearError(field: string): void {
|
||||
delete errors[field]
|
||||
}
|
||||
|
||||
/** Vide toutes les erreurs (a appeler en debut de submit). */
|
||||
function clearErrors(): void {
|
||||
for (const key of Object.keys(errors)) {
|
||||
delete errors[key]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe les violations 422 d'un payload sur les champs. Retourne true des
|
||||
* qu'au moins une violation a ete posee, false sinon (payload sans
|
||||
* violation exploitable).
|
||||
*/
|
||||
function setServerErrors(data: unknown): boolean {
|
||||
const mapped = mapViolationsToRecord(data)
|
||||
const keys = Object.keys(mapped)
|
||||
if (keys.length === 0) return false
|
||||
for (const key of keys) {
|
||||
errors[key] = mapped[key]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Route une erreur API : 422 avec violations exploitables → mapping inline
|
||||
* (pas de toast, l'erreur s'affiche sous le champ) ; sinon → toast de
|
||||
* fallback (message serveur extrait, ou `fallbackMessage`).
|
||||
*
|
||||
* Retourne true si l'erreur a ete mappee inline, false si fallback toast.
|
||||
*/
|
||||
function handleApiError(e: unknown, opts: HandleApiErrorOptions = {}): boolean {
|
||||
const status = (e as ApiFetchError)?.response?.status
|
||||
const data = (e as ApiFetchError)?.response?._data
|
||||
|
||||
if (status === 422 && setServerErrors(data)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const message
|
||||
= extractApiErrorMessage(data)
|
||||
|| opts.fallbackMessage
|
||||
|| 'Une erreur est survenue.'
|
||||
toast.error({ title: 'Erreur', message })
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
hasErrors,
|
||||
setError,
|
||||
clearError,
|
||||
clearErrors,
|
||||
setServerErrors,
|
||||
handleApiError,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user