Merge remote-tracking branch 'origin/develop' into feat/ERP-71-sidebar-commerciale-top
# Conflicts: # frontend/modules/catalog/composables/useCategoryForm.ts # frontend/modules/commercial/pages/clients/[id]/edit.vue # frontend/modules/commercial/pages/clients/new.vue # frontend/shared/composables/__tests__/useFormErrors.test.ts # frontend/shared/composables/useFormErrors.ts
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.78'
|
app.version: '0.1.79'
|
||||||
|
|||||||
@@ -228,7 +228,10 @@
|
|||||||
},
|
},
|
||||||
"sites": {
|
"sites": {
|
||||||
"notAuthorized": "Vous n'êtes pas autorisé à sélectionner ce site."
|
"notAuthorized": "Vous n'êtes pas autorisé à sélectionner ce site."
|
||||||
}
|
},
|
||||||
|
"title": "Erreur",
|
||||||
|
"generic": "Une erreur est survenue.",
|
||||||
|
"unknown": "Erreur inconnue."
|
||||||
},
|
},
|
||||||
"sites": {
|
"sites": {
|
||||||
"selector": {
|
"selector": {
|
||||||
@@ -285,7 +288,8 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"logout": "Deconnexion reussie"
|
"logout": "Deconnexion reussie"
|
||||||
}
|
},
|
||||||
|
"title": "Succès"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"roles": {
|
"roles": {
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ describe('useCategoryForm', () => {
|
|||||||
await form.submitCreate()
|
await form.submitCreate()
|
||||||
|
|
||||||
expect(mockToastSuccess).toHaveBeenCalledWith({
|
expect(mockToastSuccess).toHaveBeenCalledWith({
|
||||||
title: 'Succès',
|
title: 'success.title',
|
||||||
message: 'admin.categories.toast.created',
|
message: 'admin.categories.toast.created',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -302,7 +302,7 @@ describe('useCategoryForm', () => {
|
|||||||
// Pas d'erreur inline par champ : l'erreur transverse part en toast.
|
// Pas d'erreur inline par champ : l'erreur transverse part en toast.
|
||||||
expect(form.errors).toEqual({})
|
expect(form.errors).toEqual({})
|
||||||
expect(mockToastError).toHaveBeenCalledWith({
|
expect(mockToastError).toHaveBeenCalledWith({
|
||||||
title: 'Erreur',
|
title: 'errors.title',
|
||||||
message: 'Boom server',
|
message: 'Boom server',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -378,7 +378,7 @@ describe('useCategoryForm', () => {
|
|||||||
await form.submitUpdate(42)
|
await form.submitUpdate(42)
|
||||||
|
|
||||||
expect(mockToastSuccess).toHaveBeenCalledWith({
|
expect(mockToastSuccess).toHaveBeenCalledWith({
|
||||||
title: 'Succès',
|
title: 'success.title',
|
||||||
message: 'admin.categories.toast.updated',
|
message: 'admin.categories.toast.updated',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -409,7 +409,7 @@ describe('useCategoryForm', () => {
|
|||||||
expect(mockDelete).toHaveBeenCalledWith('/categories/42', {}, { toast: false })
|
expect(mockDelete).toHaveBeenCalledWith('/categories/42', {}, { toast: false })
|
||||||
expect(ok).toBe(true)
|
expect(ok).toBe(true)
|
||||||
expect(mockToastSuccess).toHaveBeenCalledWith({
|
expect(mockToastSuccess).toHaveBeenCalledWith({
|
||||||
title: 'Succès',
|
title: 'success.title',
|
||||||
message: 'admin.categories.toast.deleted',
|
message: 'admin.categories.toast.deleted',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -129,11 +129,11 @@ export function useCategoryForm() {
|
|||||||
name: attemptedName,
|
name: attemptedName,
|
||||||
})
|
})
|
||||||
formErrors.setError('name', duplicateMessage)
|
formErrors.setError('name', duplicateMessage)
|
||||||
toast.error({ title: 'Erreur', message: duplicateMessage })
|
toast.error({ title: t('errors.title'), message: duplicateMessage })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return formErrors.handleApiError(e, { fallbackMessage: 'Une erreur est survenue.' })
|
return formErrors.handleApiError(e, { fallbackMessage: t('errors.generic') })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +150,7 @@ export function useCategoryForm() {
|
|||||||
toast: false,
|
toast: false,
|
||||||
})
|
})
|
||||||
toast.success({
|
toast.success({
|
||||||
title: 'Succès',
|
title: t('success.title'),
|
||||||
message: t('admin.categories.toast.created'),
|
message: t('admin.categories.toast.created'),
|
||||||
})
|
})
|
||||||
return created
|
return created
|
||||||
@@ -189,7 +189,7 @@ export function useCategoryForm() {
|
|||||||
toast: false,
|
toast: false,
|
||||||
})
|
})
|
||||||
toast.success({
|
toast.success({
|
||||||
title: 'Succès',
|
title: t('success.title'),
|
||||||
message: t('admin.categories.toast.updated'),
|
message: t('admin.categories.toast.updated'),
|
||||||
})
|
})
|
||||||
return updated
|
return updated
|
||||||
@@ -215,7 +215,7 @@ export function useCategoryForm() {
|
|||||||
try {
|
try {
|
||||||
await api.delete(`/categories/${id}`, {}, { toast: false })
|
await api.delete(`/categories/${id}`, {}, { toast: false })
|
||||||
toast.success({
|
toast.success({
|
||||||
title: 'Succès',
|
title: t('success.title'),
|
||||||
message: t('admin.categories.toast.deleted'),
|
message: t('admin.categories.toast.deleted'),
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -379,9 +379,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, type Ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useClient } from '~/modules/commercial/composables/useClient'
|
import { useClient } from '~/modules/commercial/composables/useClient'
|
||||||
import { useClientReferentials, type CategoryOption, type RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
import { useClientReferentials, type CategoryOption, type RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
||||||
|
import { useClientFormErrors } from '~/modules/commercial/composables/useClientFormErrors'
|
||||||
import {
|
import {
|
||||||
canEditClient,
|
canEditClient,
|
||||||
categoryOptionsOf,
|
categoryOptionsOf,
|
||||||
@@ -424,7 +425,7 @@ import {
|
|||||||
type ContactFormDraft,
|
type ContactFormDraft,
|
||||||
type RibFormDraft,
|
type RibFormDraft,
|
||||||
} from '~/modules/commercial/types/clientForm'
|
} from '~/modules/commercial/types/clientForm'
|
||||||
import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api'
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||||
|
|
||||||
// Masques de saisie (la normalisation finale reste serveur).
|
// Masques de saisie (la normalisation finale reste serveur).
|
||||||
const SIREN_MASK = '#########'
|
const SIREN_MASK = '#########'
|
||||||
@@ -615,34 +616,20 @@ function showError(e: unknown, opts: { duplicateCompany?: boolean } = {}): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
||||||
// Un `useFormErrors` par groupe scalaire (submit independant) + un tableau
|
// Etat d'erreurs factorise avec l'ecran de creation (cf. useClientFormErrors) :
|
||||||
// d'erreurs par ligne pour chaque collection (aligne sur l'index visible).
|
// un `useFormErrors` par groupe scalaire + un tableau d'erreurs par ligne pour
|
||||||
const mainErrors = useFormErrors()
|
// chaque collection (aligne sur l'index visible). `mapRowError` mappe une 422
|
||||||
const informationErrors = useFormErrors()
|
// inline et retourne true ; il ne toaste pas, le fallback `showError` reste
|
||||||
const accountingErrors = useFormErrors()
|
// local a l'edition (cf. catch des submits de collection).
|
||||||
const contactErrors = ref<Record<string, string>[]>([])
|
const {
|
||||||
const addressErrors = ref<Record<string, string>[]>([])
|
mainErrors,
|
||||||
const ribErrors = ref<Record<string, string>[]>([])
|
informationErrors,
|
||||||
|
accountingErrors,
|
||||||
/**
|
contactErrors,
|
||||||
* Mappe l'erreur d'une ligne de collection sur le tableau d'erreurs cible (par
|
addressErrors,
|
||||||
* index). 422 exploitable → erreurs inline sous les champs de la ligne ; sinon
|
ribErrors,
|
||||||
* → toast de fallback. Renvoie true si mappee inline.
|
mapRowError,
|
||||||
*/
|
} = useClientFormErrors()
|
||||||
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
|
|
||||||
}
|
|
||||||
showError(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Bloc principal ───────────────────────────────────────────────────────────
|
// ── Bloc principal ───────────────────────────────────────────────────────────
|
||||||
const isMainValid = computed(() => {
|
const isMainValid = computed(() => {
|
||||||
@@ -775,7 +762,9 @@ async function submitContacts(): Promise<void> {
|
|||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
// 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe.
|
// 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe.
|
||||||
mapRowError(error, contactErrors, index)
|
if (!mapRowError(error, contactErrors, index)) {
|
||||||
|
showError(error)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -852,7 +841,9 @@ async function submitAddresses(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
mapRowError(error, addressErrors, index)
|
if (!mapRowError(error, addressErrors, index)) {
|
||||||
|
showError(error)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,7 +940,9 @@ async function submitAccounting(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
mapRowError(error, ribErrors, index)
|
if (!mapRowError(error, ribErrors, index)) {
|
||||||
|
showError(error)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,8 +376,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch, type Ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useClientReferentials, type RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
import { useClientReferentials, type RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
||||||
|
import { useClientFormErrors } from '~/modules/commercial/composables/useClientFormErrors'
|
||||||
import {
|
import {
|
||||||
buildClientFormTabKeys,
|
buildClientFormTabKeys,
|
||||||
CLIENT_FORM_PLACEHOLDER_TABS,
|
CLIENT_FORM_PLACEHOLDER_TABS,
|
||||||
@@ -395,7 +396,7 @@ import {
|
|||||||
type ContactFormDraft,
|
type ContactFormDraft,
|
||||||
type RibFormDraft,
|
type RibFormDraft,
|
||||||
} from '~/modules/commercial/types/clientForm'
|
} from '~/modules/commercial/types/clientForm'
|
||||||
import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api'
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||||
|
|
||||||
// Masques de saisie (la normalisation finale reste serveur).
|
// Masques de saisie (la normalisation finale reste serveur).
|
||||||
const SIREN_MASK = '#########'
|
const SIREN_MASK = '#########'
|
||||||
@@ -428,36 +429,20 @@ function apiErrorMessage(error: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
||||||
// Un `useFormErrors` par groupe scalaire (submit independant) : les violations
|
// Etat d'erreurs factorise entre creation et edition (cf. useClientFormErrors) :
|
||||||
// 422 du serveur s'affichent sous le champ concerne (prop `:error`) au lieu d'un
|
// un `useFormErrors` par groupe scalaire (Principal / Information / Comptabilite)
|
||||||
// simple toast. Les collections (contacts/adresses/RIB) portent une erreur PAR
|
// + un tableau d'erreurs par ligne pour chaque collection (contacts/adresses/RIB).
|
||||||
// LIGNE via des tableaux alignes sur l'index, peuples par `mapRowError`.
|
// `mapRowError` mappe une 422 inline et retourne true ; il ne toaste pas, le
|
||||||
const mainErrors = useFormErrors()
|
// fallback reste local a la creation (cf. catch des submits de collection).
|
||||||
const informationErrors = useFormErrors()
|
const {
|
||||||
const accountingErrors = useFormErrors()
|
mainErrors,
|
||||||
const contactErrors = ref<Record<string, string>[]>([])
|
informationErrors,
|
||||||
const addressErrors = ref<Record<string, string>[]>([])
|
accountingErrors,
|
||||||
const ribErrors = ref<Record<string, string>[]>([])
|
contactErrors,
|
||||||
|
addressErrors,
|
||||||
/**
|
ribErrors,
|
||||||
* Mappe l'erreur d'une ligne de collection sur le tableau d'erreurs cible (par
|
mapRowError,
|
||||||
* index). 422 avec violations exploitables → erreurs inline sous les champs de
|
} = useClientFormErrors()
|
||||||
* la ligne ; sinon → toast de fallback. Renvoie true si mappee inline.
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
}
|
|
||||||
toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
useHead({ title: t('commercial.clients.form.title') })
|
useHead({ title: t('commercial.clients.form.title') })
|
||||||
|
|
||||||
@@ -724,7 +709,9 @@ async function submitContacts(): Promise<void> {
|
|||||||
catch (error) {
|
catch (error) {
|
||||||
// 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe
|
// 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe
|
||||||
// a la premiere ligne en echec (les suivantes ne sont pas tentees).
|
// a la premiere ligne en echec (les suivantes ne sont pas tentees).
|
||||||
mapRowError(error, contactErrors, index)
|
if (!mapRowError(error, contactErrors, index)) {
|
||||||
|
toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -830,7 +817,9 @@ async function submitAddresses(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
mapRowError(error, addressErrors, index)
|
if (!mapRowError(error, addressErrors, index)) {
|
||||||
|
toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -941,7 +930,9 @@ async function submitAccounting(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
mapRowError(error, ribErrors, index)
|
if (!mapRowError(error, ribErrors, index)) {
|
||||||
|
toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useFormErrors } from '../useFormErrors'
|
|||||||
|
|
||||||
const mockToastError = vi.hoisted(() => vi.fn())
|
const mockToastError = vi.hoisted(() => vi.fn())
|
||||||
vi.stubGlobal('useToast', () => ({ error: mockToastError, success: vi.fn() }))
|
vi.stubGlobal('useToast', () => ({ error: mockToastError, success: vi.fn() }))
|
||||||
|
// useI18n stub : renvoie la cle telle quelle (pour asserter dessus).
|
||||||
|
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests du composable `useFormErrors` — pendant front de la regle « le back
|
* Tests du composable `useFormErrors` — pendant front de la regle « le back
|
||||||
@@ -76,7 +78,8 @@ describe('useFormErrors', () => {
|
|||||||
expect(handled).toBe(false)
|
expect(handled).toBe(false)
|
||||||
expect(errors).toEqual({})
|
expect(errors).toEqual({})
|
||||||
expect(mockToastError).toHaveBeenCalledTimes(1)
|
expect(mockToastError).toHaveBeenCalledTimes(1)
|
||||||
expect(mockToastError.mock.calls[0][0]).toMatchObject({ message: 'Erreur serveur.' })
|
// Titre via i18n (cle renvoyee telle quelle par le stub).
|
||||||
|
expect(mockToastError.mock.calls[0][0]).toMatchObject({ title: 'errors.title', message: 'Erreur serveur.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handleApiError : 422 sans violation mappable → toast de fallback, retourne false', () => {
|
it('handleApiError : 422 sans violation mappable → toast de fallback, retourne false', () => {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useApi(): ApiClient {
|
|||||||
const data = responseData ?? (error as FetchError)?.data
|
const data = responseData ?? (error as FetchError)?.data
|
||||||
const msg = extractApiErrorMessage(data)
|
const msg = extractApiErrorMessage(data)
|
||||||
if (msg) return msg
|
if (msg) return msg
|
||||||
return (error as FetchError)?.message ?? 'Erreur inconnue.'
|
return (error as FetchError)?.message ?? t('errors.unknown')
|
||||||
}
|
}
|
||||||
|
|
||||||
const methodErrorKeys: Record<string, string> = {
|
const methodErrorKeys: Record<string, string> = {
|
||||||
@@ -76,7 +76,7 @@ export function useApi(): ApiClient {
|
|||||||
|
|
||||||
if (successMessage) {
|
if (successMessage) {
|
||||||
toast.success({
|
toast.success({
|
||||||
title: 'Succes',
|
title: t('success.title'),
|
||||||
message: successMessage
|
message: successMessage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -98,10 +98,10 @@ export function useApi(): ApiClient {
|
|||||||
apiOptions?.toastErrorMessage ||
|
apiOptions?.toastErrorMessage ||
|
||||||
errorMessage ||
|
errorMessage ||
|
||||||
extractedMessage ||
|
extractedMessage ||
|
||||||
'Une erreur est survenue.'
|
t('errors.generic')
|
||||||
|
|
||||||
toast.error({
|
toast.error({
|
||||||
title: apiOptions?.toastTitle ?? 'Erreur',
|
title: apiOptions?.toastTitle ?? t('errors.title'),
|
||||||
message
|
message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export function useApi(): ApiClient {
|
|||||||
'Une erreur est survenue.'
|
'Une erreur est survenue.'
|
||||||
|
|
||||||
toast.error({
|
toast.error({
|
||||||
title: apiOptions?.toastTitle ?? 'Erreur',
|
title: apiOptions?.toastTitle ?? t('errors.title'),
|
||||||
message
|
message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface HandleApiErrorOptions {
|
|||||||
|
|
||||||
export function useFormErrors() {
|
export function useFormErrors() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Etat d'erreurs indexe par propertyPath. Reactif : muter une cle suffit a
|
// Etat d'erreurs indexe par propertyPath. Reactif : muter une cle suffit a
|
||||||
// rafraichir la prop `:error` du champ correspondant.
|
// rafraichir la prop `:error` du champ correspondant.
|
||||||
@@ -95,8 +96,8 @@ export function useFormErrors() {
|
|||||||
const message
|
const message
|
||||||
= extractApiErrorMessage(data)
|
= extractApiErrorMessage(data)
|
||||||
|| opts.fallbackMessage
|
|| opts.fallbackMessage
|
||||||
|| 'Une erreur est survenue.'
|
|| t('errors.generic')
|
||||||
toast.error({ title: 'Erreur', message })
|
toast.error({ title: t('errors.title'), message })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user