feat(front) : mapping des erreurs de validation 422 par champ (ERP-101) (#58)
Auto Tag Develop / tag (push) Successful in 7s
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>
This commit was merged in pull request #58.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
||||
import { useClientFormErrors } from '../useClientFormErrors'
|
||||
|
||||
// useFormErrors (auto-import) expose l'implementation reelle ; elle consomme
|
||||
// useToast + useI18n, stubbes ici.
|
||||
vi.stubGlobal('useToast', () => ({ error: vi.fn(), success: vi.fn() }))
|
||||
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
||||
vi.stubGlobal('useFormErrors', useFormErrors)
|
||||
|
||||
/**
|
||||
* Tests du composable partage `useClientFormErrors` — factorisation du cablage
|
||||
* d'erreurs des ecrans client (creation/edition), suggestion de revue ERP-101.
|
||||
* `mapRowError` ne toaste plus : il retourne un booleen et chaque page garde son
|
||||
* propre fallback (toast.error en creation, showError en edition).
|
||||
*/
|
||||
describe('useClientFormErrors', () => {
|
||||
it('expose les 3 etats scalaires (vides) et les 3 tableaux d\'erreurs par ligne', () => {
|
||||
const f = useClientFormErrors()
|
||||
expect(f.mainErrors.errors).toEqual({})
|
||||
expect(f.informationErrors.errors).toEqual({})
|
||||
expect(f.accountingErrors.errors).toEqual({})
|
||||
expect(f.contactErrors.value).toEqual([])
|
||||
expect(f.addressErrors.value).toEqual([])
|
||||
expect(f.ribErrors.value).toEqual([])
|
||||
})
|
||||
|
||||
it('mapRowError mappe une 422 sur target[index] et retourne true', () => {
|
||||
const f = useClientFormErrors()
|
||||
const error = {
|
||||
response: {
|
||||
status: 422,
|
||||
_data: { violations: [{ propertyPath: 'email', message: 'Adresse invalide.' }] },
|
||||
},
|
||||
}
|
||||
const mapped = f.mapRowError(error, f.contactErrors, 0)
|
||||
expect(mapped).toBe(true)
|
||||
expect(f.contactErrors.value[0]).toEqual({ email: 'Adresse invalide.' })
|
||||
})
|
||||
|
||||
it('mapRowError retourne false et ne touche pas la cible pour une erreur non-422', () => {
|
||||
const f = useClientFormErrors()
|
||||
const error = { response: { status: 500, _data: {} } }
|
||||
expect(f.mapRowError(error, f.ribErrors, 0)).toBe(false)
|
||||
expect(f.ribErrors.value[0]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('mapRowError retourne false pour une 422 sans violation exploitable', () => {
|
||||
const f = useClientFormErrors()
|
||||
const error = { response: { status: 422, _data: { 'hydra:description': 'Donnees invalides.' } } }
|
||||
expect(f.mapRowError(error, f.addressErrors, 0)).toBe(false)
|
||||
expect(f.addressErrors.value[0]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Composable d'erreurs partage des ecrans client (creation + edition, M1
|
||||
* Commercial). Factorise le cablage identique entre `clients/new.vue` et
|
||||
* `clients/[id]/edit.vue` (suggestion de revue ERP-101) :
|
||||
* - un `useFormErrors` par groupe scalaire (Principal / Information /
|
||||
* Comptabilite) : violations 422 affichees inline sous chaque champ ;
|
||||
* - un tableau d'erreurs PAR LIGNE pour chaque collection (contacts /
|
||||
* adresses / RIB), aligne sur l'index du `v-for`.
|
||||
*
|
||||
* `mapRowError` ne toaste PAS lui-meme : il retourne un booleen (true = mappe
|
||||
* inline). Chaque page conserve ainsi son propre fallback dans le `catch`
|
||||
* (toast generique en creation, `showError` en edition) sans imposer un
|
||||
* comportement commun.
|
||||
*/
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { mapViolationsToRecord } from '~/shared/utils/api'
|
||||
|
||||
export function useClientFormErrors() {
|
||||
const mainErrors = useFormErrors()
|
||||
const informationErrors = useFormErrors()
|
||||
const accountingErrors = useFormErrors()
|
||||
const contactErrors = ref<Record<string, string>[]>([])
|
||||
const addressErrors = ref<Record<string, string>[]>([])
|
||||
const ribErrors = ref<Record<string, string>[]>([])
|
||||
|
||||
/**
|
||||
* Mappe l'erreur d'une ligne de collection sur le tableau cible (par index).
|
||||
* 422 avec violations exploitables → erreurs inline sous les champs de la
|
||||
* ligne + retourne true. Sinon → ne touche pas la cible et retourne false
|
||||
* (le caller decide du fallback toast).
|
||||
*/
|
||||
function mapRowError(
|
||||
error: unknown,
|
||||
target: Ref<Record<string, string>[]>,
|
||||
index: number,
|
||||
): boolean {
|
||||
const response = (error as { response?: { status?: number, _data?: unknown } })?.response
|
||||
const mapped = response?.status === 422 ? mapViolationsToRecord(response._data) : {}
|
||||
if (Object.keys(mapped).length > 0) {
|
||||
target.value[index] = mapped
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
mainErrors,
|
||||
informationErrors,
|
||||
accountingErrors,
|
||||
contactErrors,
|
||||
addressErrors,
|
||||
ribErrors,
|
||||
mapRowError,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user