ee3bbea649
Auto Tag Develop / tag (push) Successful in 7s
## 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>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
/**
|
|
* 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()
|
|
const { t } = useI18n()
|
|
|
|
// 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
|
|
|| t('errors.generic')
|
|
toast.error({ title: t('errors.title'), message })
|
|
return false
|
|
}
|
|
|
|
return {
|
|
errors,
|
|
hasErrors,
|
|
setError,
|
|
clearError,
|
|
clearErrors,
|
|
setServerErrors,
|
|
handleApiError,
|
|
}
|
|
}
|