Skip to content

Commit cbf56ab

Browse files
committed
feat: enhance template error handling with field-specific validation messages
1 parent 88c00fc commit cbf56ab

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

assets/vue/views/TemplateEditView.vue

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@
121121
</div>
122122
</div>
123123

124-
<div v-if="saveError" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
124+
<div v-if="saveErrors.length" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
125+
<p class="font-medium">Please fix the following fields:</p>
126+
<ul class="mt-1 list-disc pl-5 space-y-1">
127+
<li v-for="errorItem in saveErrors" :key="errorItem">{{ errorItem }}</li>
128+
</ul>
129+
</div>
130+
<div v-else-if="saveError" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
125131
{{ saveError }}
126132
</div>
127133
<div v-if="saveSuccess" class="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
@@ -164,6 +170,7 @@ const isLoading = ref(false)
164170
const loadError = ref('')
165171
const isSaving = ref(false)
166172
const saveError = ref('')
173+
const saveErrors = ref([])
167174
const saveSuccess = ref('')
168175
const form = ref({
169176
title: '',
@@ -240,19 +247,63 @@ const handleFileChange = (event) => {
240247
form.value.file = file || null
241248
}
242249
250+
const validationFieldLabels = {
251+
title: 'Title',
252+
content: 'Content',
253+
text: 'Text version',
254+
file: 'Template file',
255+
list_order: 'List order',
256+
check_links: 'Check links',
257+
check_images: 'Check images',
258+
check_external_images: 'Check external images'
259+
}
260+
261+
const normalizeFieldName = (fieldPath = '') => {
262+
if (validationFieldLabels[fieldPath]) return validationFieldLabels[fieldPath]
263+
264+
const fallback = String(fieldPath)
265+
.split('.')
266+
.pop()
267+
?.replace(/\[\d+]/g, '')
268+
?.replace(/_/g, ' ')
269+
?.replace(/([a-z])([A-Z])/g, '$1 $2')
270+
?.trim()
271+
272+
if (!fallback) return 'Field'
273+
return fallback.charAt(0).toUpperCase() + fallback.slice(1)
274+
}
275+
276+
const formatValidationErrors = (error) => {
277+
const responseData = error?.responseData
278+
const messages = []
279+
280+
if (responseData && typeof responseData === 'object' && !Array.isArray(responseData)) {
281+
Object.entries(responseData).forEach(([field, rawMessage]) => {
282+
if (!rawMessage) return
283+
const text = Array.isArray(rawMessage) ? rawMessage.join(' ') : String(rawMessage)
284+
messages.push(`${normalizeFieldName(field)}: ${text}`)
285+
})
286+
}
287+
288+
return [...new Set(messages)]
289+
}
290+
243291
const saveTemplate = async () => {
244292
if (!isCreateMode.value && (!Number.isFinite(templateId.value) || templateId.value <= 0)) {
245293
saveError.value = 'Template ID is invalid.'
294+
saveErrors.value = []
246295
return
247296
}
248297
249298
if (!form.value.title) {
250299
saveError.value = 'Title is required.'
300+
saveErrors.value = []
251301
return
252302
}
253303
254304
isSaving.value = true
255305
saveError.value = ''
306+
saveErrors.value = []
256307
saveSuccess.value = ''
257308
258309
try {
@@ -292,7 +343,14 @@ const saveTemplate = async () => {
292343
saveSuccess.value = 'Template updated successfully.'
293344
} catch (error) {
294345
console.error('Failed to save template:', error)
295-
saveError.value = error?.message || 'Failed to save template.'
346+
const formattedErrors = formatValidationErrors(error)
347+
if (formattedErrors.length > 0) {
348+
saveErrors.value = formattedErrors
349+
saveError.value = ''
350+
} else {
351+
saveError.value = error?.message || 'Failed to save template.'
352+
saveErrors.value = []
353+
}
296354
} finally {
297355
isSaving.value = false
298356
}

tests/Unit/assets/vue/views/TemplateEditorView.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ describe('TemplateEditView', () => {
9797
)
9898
})
9999

100+
it('shows field-specific errors returned by the API', async () => {
101+
vi.spyOn(api.templateClient, 'updateTemplate').mockRejectedValue({
102+
name: 'ValidationException',
103+
message: 'Validation failed',
104+
responseData: {
105+
title: ['This value is too long.'],
106+
list_order: ['This value should be a valid number.'],
107+
},
108+
})
109+
110+
const wrapper = await mountComponent()
111+
112+
await wrapper.find('#template-title').setValue('Updated Template')
113+
await wrapper.find('form').trigger('submit.prevent')
114+
await flushPromises()
115+
116+
expect(wrapper.text()).toContain('Title: This value is too long.')
117+
expect(wrapper.text()).toContain('List order: This value should be a valid number.')
118+
expect(wrapper.text()).not.toContain('Validation failed')
119+
})
120+
100121
it('creates a template in create mode', async () => {
101122
mockRoute.name = 'template-create'
102123
mockRoute.params.templateId = ''

0 commit comments

Comments
 (0)