Skip to content

Commit 114efd5

Browse files
committed
BUG #167
1 parent be5e579 commit 114efd5

7 files changed

Lines changed: 149 additions & 38 deletions

File tree

apps/web/src/components/core/jsonforms-renderer.vue

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,28 @@ export default defineNuxtComponent({
162162
}
163163
},
164164
methods: {
165+
/**
166+
* Restreint une map de validations « a plat » au schema courant.
167+
*
168+
* `DtoValidationPipe` renvoie des cles prefixees par le chemin complet du champ
169+
* (`inetOrgPerson.cn`, `additionalFields.attributes.people.uid`) alors que chaque
170+
* renderer travaille sur un sous-objet de l'identite. Sans retrait du prefixe,
171+
* l'`instancePath` produit ne correspond a aucun controle et l'erreur n'est pas affichee.
172+
*/
173+
scopeFlatValidations(validations: Record<string, unknown>, schemaName: string): Record<string, unknown> {
174+
const suffix = `${schemaName}.`
175+
const scoped: Record<string, unknown> = {}
176+
177+
for (const [key, value] of Object.entries(validations)) {
178+
const index = key.indexOf(suffix)
179+
// Le nom du schema doit etre un segment complet du chemin, pas une sous-chaine d'un champ.
180+
if (index === 0 || (index > 0 && key[index - 1] === '.')) {
181+
scoped[key.slice(index + suffix.length)] = value
182+
}
183+
}
184+
185+
return scoped
186+
},
165187
onChange(event: any) {
166188
this.$emit('update:modelValue', event.data)
167189
@@ -192,7 +214,11 @@ export default defineNuxtComponent({
192214
i18n() {
193215
return {
194216
locale: 'fr',
195-
translate: computed(() => this.createTranslator('fr')),
217+
// `translate` doit etre la fonction elle-meme : JSON Forms l'appelle directement
218+
// (`t(key, defaultMessage, context)`) des qu'un controle porte une erreur. Enveloppee
219+
// dans un `computed`, l'appel levait une TypeError avalee par `errorCaptured`,
220+
// et le champ en defaut n'etait jamais signale.
221+
translate: this.createTranslator('fr'),
196222
}
197223
},
198224
getSchemaValidations(): any[] {
@@ -205,7 +231,7 @@ export default defineNuxtComponent({
205231
: hasSchemaScopedValidation
206232
? rootValidations[this.schemaName]
207233
: isFlatValidationMap
208-
? rootValidations
234+
? this.scopeFlatValidations(rootValidations, this.schemaName)
209235
: {}
210236
211237
const entries: Array<{ path: string; message: string }> = []

apps/web/src/components/pages/identities/schemas-bar.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,18 @@ export default defineNuxtComponent({
171171
return false
172172
}
173173
174-
return hasValidation(this.validations?.[tab]) ? 'red' : false
174+
// Format imbrique (`{ people: { uid: '...' } }`) renvoye par la validation de schema.
175+
if (hasValidation(this.validations?.[tab])) return 'red'
176+
177+
// Format a plat a cles pointees (`{ 'inetOrgPerson.cn': '...' }`) renvoye par le DTO :
178+
// le nom de l'onglet doit y etre un segment complet du chemin.
179+
const suffix = `${tab}.`
180+
const matchesTab = Object.entries(this.validations || {}).some(([key, value]) => {
181+
const index = key.indexOf(suffix)
182+
return (index === 0 || (index > 0 && key[index - 1] === '.')) && hasValidation(value)
183+
})
184+
185+
return matchesTab ? 'red' : false
175186
},
176187
addSchema(schema) {
177188
if (!this.identity.additionalFields) {

apps/web/src/composables/useErrorHandling.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type handleErrorPayload = {
1212
message?: string
1313
}
1414

15-
function extractErrorMessage(error: any): string | undefined {
15+
export function extractErrorMessage(error: any): string | undefined {
1616
const raw = error?.response?._data?.message ?? error?.data?.message ?? error?.cause?.response?._data?.message ?? error?.message
1717
if (Array.isArray(raw)) {
1818
const joined = raw.map((item) => `${item}`.trim()).filter(Boolean).join(', ')
@@ -25,6 +25,59 @@ function extractErrorMessage(error: any): string | undefined {
2525
return undefined
2626
}
2727

28+
/**
29+
* Recupere la map `validations` d'une reponse API.
30+
*
31+
* Selon l'appelant (`$http` direct, `useHttp`/`useAsyncData`, erreur re-emise), la charge utile
32+
* est accessible via `response._data`, `data`, ou la meme chose sous `cause` : on teste les
33+
* quatre emplacements, comme le fait deja `extractErrorMessage`.
34+
*/
35+
export function extractValidations(error: any): Record<string, unknown> | undefined {
36+
const validations =
37+
error?.response?._data?.validations ??
38+
error?.data?.validations ??
39+
error?.cause?.response?._data?.validations ??
40+
error?.cause?.data?.validations
41+
42+
return validations && typeof validations === 'object' ? validations : undefined
43+
}
44+
45+
/**
46+
* Construit un message d'erreur lisible a partir d'une reponse API :
47+
* le `message` renvoye par l'API, complete des `validations` par champ quand elles existent.
48+
*/
49+
export function formatApiErrorMessage(error: any, fallback: string): string {
50+
const parts: string[] = []
51+
const message = extractErrorMessage(error)
52+
if (message) parts.push(message)
53+
54+
const validations = extractValidations(error)
55+
if (validations) {
56+
for (const [field, detail] of Object.entries(validations)) {
57+
if (field === 'message') continue
58+
const text = typeof detail === 'string' ? detail : flattenValidationDetail(detail)
59+
if (text) parts.push(`${field} : ${text}`)
60+
}
61+
}
62+
63+
return parts.length ? parts.join(' - ') : fallback
64+
}
65+
66+
function flattenValidationDetail(detail: any): string {
67+
if (typeof detail === 'string') return detail
68+
if (Array.isArray(detail)) return detail.map(flattenValidationDetail).filter(Boolean).join(', ')
69+
if (detail && typeof detail === 'object') {
70+
return Object.entries(detail)
71+
.map(([key, value]) => {
72+
const text = flattenValidationDetail(value)
73+
return text ? `${key} : ${text}` : ''
74+
})
75+
.filter(Boolean)
76+
.join(', ')
77+
}
78+
return ''
79+
}
80+
2881
export function useErrorHandling(): useErrorHandlingReturnType {
2982
function handleMfaRequiredIfNeeded(error: any): boolean {
3083
const statusCode = error?.response?._data?.statusCode || error?.data?.statusCode

apps/web/src/pages/identities/table/[_id].vue

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ q-card.flex.column.fit.absolute(flat)
3939
</template>
4040

4141
<script lang="ts">
42+
import { extractValidations, formatApiErrorMessage } from '~/composables/useErrorHandling'
4243
import { clone } from 'radash'
4344
import { IdentityState } from '~/constants/enums'
4445
import { useIdentityStateStore } from '~/stores/identityState'
@@ -71,7 +72,9 @@ export default defineNuxtComponent({
7172
7273
if (NewTargetId === $route.params._id) {
7374
return {
74-
identity: {
75+
// `ref` obligatoire : un objet litteral renvoye par `setup` n'est pas reactif,
76+
// les erreurs de validation posees sur `additionalFields.validations` ne seraient pas affichees.
77+
identity: ref({
7578
state: IdentityState.TO_CREATE,
7679
inetOrgPerson: {
7780
mail: '',
@@ -80,8 +83,9 @@ export default defineNuxtComponent({
8083
additionalFields: {
8184
attributes: {},
8285
objectClasses: [] as string[],
86+
validations: {},
8387
},
84-
} as Identity,
88+
} as Identity),
8589
originTarget,
8690
refresh: () => Promise.resolve(),
8791
toPathWithQueries,
@@ -168,14 +172,17 @@ export default defineNuxtComponent({
168172
this.$emit('refresh')
169173
} catch (error: any) {
170174
this.$q.notify({
171-
message: "Erreur lors de la sauvegarde de l'identité",
175+
message: formatApiErrorMessage(error, "Erreur lors de la sauvegarde de l'identité"),
172176
color: 'negative',
173177
position: 'top-right',
174178
icon: 'mdi-alert-circle-outline',
179+
multiLine: true,
180+
timeout: 10000,
175181
})
176182
console.error('Erreur lors de la sauvegarde de l identité:', error)
177183
178-
if (error?.response?._data?.validations) {
184+
const validations = extractValidations(error)
185+
if (validations) {
179186
if (!this.identity.additionalFields) {
180187
this.identity.additionalFields = {
181188
attributes: {},
@@ -184,13 +191,7 @@ export default defineNuxtComponent({
184191
}
185192
}
186193
187-
if (!this.identity.additionalFields.validations) {
188-
this.identity.additionalFields.validations = {}
189-
}
190-
191-
this.identity.additionalFields.validations = {
192-
...error.response._data.validations,
193-
}
194+
this.identity.additionalFields.validations = { ...validations }
194195
}
195196
}
196197
},

apps/web/src/pages/identities/table/[_id]/index.vue

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
.column.no-wrap.full-height.relative
33
.sesame-sticky-space
44
q-toolbar.bg-transparent.q-pr-none.sesame-sticky-bar
5-
q-btn.sesame.infinite.animated.flash(size="sm" padding="xs" color="negative" @click="validationsModal = true" v-if="!isNew && hasValidations" outline)
5+
q-btn.sesame.infinite.animated.flash(size="sm" padding="xs" color="negative" @click="validationsModal = true" v-if="hasValidations" outline)
66
q-tooltip.text-body2(slot="trigger") Afficher les erreurs
77
q-icon.text-negative(name='mdi-alert-box')
88
q-dialog(v-model="validationsModal")
@@ -12,10 +12,10 @@
1212
div.text-h6.q-ml-md Erreurs de validation
1313
q-card-section.q-py-sm
1414
q-list(separator)
15-
q-item(v-for="field in Object.keys(validations)" :key="field")
15+
q-item(v-for="entry in validationEntries" :key="entry.field")
1616
q-item-section.text-negative
17-
q-item-label {{ field }}
18-
q-item-label(v-for='f in validations[field]' caption) - {{ f }}
17+
q-item-label {{ entry.field }}
18+
q-item-label(v-for='message in entry.messages' :key="message" caption) - {{ message }}
1919
q-card-actions(align="right")
2020
q-btn(flat label="Fermer" color="primary" v-close-popup)
2121
q-toolbar-title.gt-xs Fiche identité
@@ -290,15 +290,27 @@ export default defineNuxtComponent({
290290
employeeType: this.identity?.inetOrgPerson?.employeeType,
291291
}
292292
},
293-
hasValidations() {
294-
if (this.validations) {
295-
for (const field in this.validations) {
296-
if (Object.keys(this.validations[field]).length > 0) {
297-
return true
298-
}
293+
/**
294+
* Aplatit les deux formes de `validations` renvoyees par l'API :
295+
* une map imbriquee par classe d'objet (`{ people: { uid: '...' } }`) issue de la validation
296+
* de schema, ou une map a plat a cles pointees (`{ 'inetOrgPerson.cn': '...' }`) issue du DTO.
297+
*/
298+
validationEntries(): Array<{ field: string; messages: string[] }> {
299+
const flatten = (value: unknown): string[] => {
300+
if (typeof value === 'string') return value.trim() ? [value.trim()] : []
301+
if (Array.isArray(value)) return value.flatMap(flatten)
302+
if (value && typeof value === 'object') {
303+
return Object.entries(value).flatMap(([key, child]) => flatten(child).map((message) => `${key} : ${message}`))
299304
}
305+
return []
300306
}
301-
return false
307+
308+
return Object.entries(this.validations)
309+
.map(([field, value]) => ({ field, messages: flatten(value) }))
310+
.filter((entry) => entry.messages.length > 0)
311+
},
312+
hasValidations() {
313+
return this.validationEntries.length > 0
302314
},
303315
getStatusColor() {
304316
if (this.identity.dataStatus === 1) {

apps/web/src/pages/identities/trash/[_id].vue

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ q-card.flex.column.fit.absolute(flat)
7070
</template>
7171

7272
<script lang="ts">
73+
import { extractValidations, formatApiErrorMessage } from '~/composables/useErrorHandling'
7374
import { useIdentityStateStore } from '~/stores/identityState'
7475
7576
export default defineNuxtComponent({
@@ -179,21 +180,18 @@ export default defineNuxtComponent({
179180
this.$emit('refresh')
180181
} catch (error: any) {
181182
this.$q.notify({
182-
message: "Erreur lors de la sauvegarde de l'identité",
183+
message: formatApiErrorMessage(error, "Erreur lors de la sauvegarde de l'identité"),
183184
color: 'negative',
184185
position: 'top-right',
185186
icon: 'mdi-alert-circle-outline',
187+
multiLine: true,
188+
timeout: 10000,
186189
})
187190
console.error('Erreur lors de la sauvegarde de l identité:', error)
188191
189-
if (error?.response?._data?.validations) {
190-
if (!this.identity?.additionalFields?.validations) {
191-
this.identity.additionalFields.validations = {}
192-
}
193-
194-
for (const v in error.response._data.validations) {
195-
this.identity.additionalFields.validations[v] = error.response._data.validations[v]
196-
}
192+
const validations = extractValidations(error)
193+
if (validations && this.identity.additionalFields) {
194+
this.identity.additionalFields.validations = { ...validations }
197195
}
198196
}
199197
},

apps/web/src/server/routes/config/ui.get.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ async function readYamlFile(filePath: string): Promise<Record<string, unknown>>
6060
}
6161
}
6262

63+
/**
64+
* Lit un fichier de `./config`, avec repli sur `./default` : en dev local
65+
* `scripts/checkinstall.sh` (qui peuple `./config`) n'est pas exécuté.
66+
*/
67+
async function readConfigYamlFile(fileName: string): Promise<Record<string, unknown>> {
68+
const fromConfig = await readYamlFile(`./config/${fileName}`)
69+
if (Object.keys(fromConfig).length > 0) return fromConfig
70+
return readYamlFile(`./default/${fileName}`)
71+
}
72+
6373
let defaultMenuEntriesJsonGenerated = false
6474

6575
async function ensureDefaultMenuDataJson(): Promise<void> {
@@ -95,9 +105,9 @@ async function ensureDefaultMenuDataJson(): Promise<void> {
95105
export default defineEventHandler(async () => {
96106
await ensureDefaultMenuDataJson()
97107

98-
const menusFile = await readYamlFile('./config/menus.yml')
99-
const identitiesColumnsFile = await readYamlFile('./config/identities-columns.yml')
100-
const identitiesSearchFieldsFile = await readYamlFile('./config/identities-search-fields.yml')
108+
const menusFile = await readConfigYamlFile('menus.yml')
109+
const identitiesColumnsFile = await readConfigYamlFile('identities-columns.yml')
110+
const identitiesSearchFieldsFile = await readConfigYamlFile('identities-search-fields.yml')
101111

102112
return {
103113
menus: {

0 commit comments

Comments
 (0)