9595 v-model.trim =" createForm.regex"
9696 type =" text"
9797 required
98- class =" mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
98+ :class =" [
99+ 'mt-1 block w-full rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm',
100+ fieldHasError('regex')
101+ ? 'border border-red-300 focus:ring-red-500 focus:border-red-500'
102+ : 'border border-slate-300 focus:ring-blue-500 focus:border-blue-500'
103+ ]"
99104 >
105+ <p
106+ v-for =" message in fieldErrors('regex')"
107+ :key =" `regex-${message}`"
108+ class =" mt-1 text-sm text-red-600"
109+ >
110+ {{ message }}
111+ </p >
100112 </div >
101113
102114 <div >
105117 id =" bounce-rule-comment"
106118 v-model.trim =" createForm.comment"
107119 type =" text"
108- class =" mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
120+ :class =" [
121+ 'mt-1 block w-full rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm',
122+ fieldHasError('comment')
123+ ? 'border border-red-300 focus:ring-red-500 focus:border-red-500'
124+ : 'border border-slate-300 focus:ring-blue-500 focus:border-blue-500'
125+ ]"
126+ >
127+ <p
128+ v-for =" message in fieldErrors('comment')"
129+ :key =" `comment-${message}`"
130+ class =" mt-1 text-sm text-red-600"
109131 >
132+ {{ message }}
133+ </p >
110134 </div >
111135
112136 <div class =" grid grid-cols-1 sm:grid-cols-2 gap-4" >
115139 <select
116140 id =" bounce-rule-action"
117141 v-model =" createForm.action"
118- class =" mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
142+ :class =" [
143+ 'mt-1 block w-full rounded-md shadow-sm py-2 px-3 bg-white focus:outline-none sm:text-sm',
144+ fieldHasError('action')
145+ ? 'border border-red-300 focus:ring-red-500 focus:border-red-500'
146+ : 'border border-slate-300 focus:ring-blue-500 focus:border-blue-500'
147+ ]"
119148 >
120149 <option v-for =" bounceAction in bounceActions" :key =" bounceAction" :value =" bounceAction" >{{ bounceAction }}</option >
121150 </select >
151+ <p
152+ v-for =" message in fieldErrors('action')"
153+ :key =" `action-${message}`"
154+ class =" mt-1 text-sm text-red-600"
155+ >
156+ {{ message }}
157+ </p >
122158 </div >
123159
124160 <div >
125161 <label for =" bounce-rule-status" class =" block text-sm font-medium text-slate-700" >Status</label >
126162 <select
127163 id =" bounce-rule-status"
128164 v-model =" createForm.status"
129- class =" mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
165+ :class =" [
166+ 'mt-1 block w-full rounded-md shadow-sm py-2 px-3 bg-white focus:outline-none sm:text-sm',
167+ fieldHasError('status')
168+ ? 'border border-red-300 focus:ring-red-500 focus:border-red-500'
169+ : 'border border-slate-300 focus:ring-blue-500 focus:border-blue-500'
170+ ]"
130171 >
131172 <option value =" active" >active</option >
132173 <option value =" inactive" >inactive</option >
133174 </select >
175+ <p
176+ v-for =" message in fieldErrors('status')"
177+ :key =" `status-${message}`"
178+ class =" mt-1 text-sm text-red-600"
179+ >
180+ {{ message }}
181+ </p >
134182 </div >
135183 </div >
136184
142190 type =" number"
143191 min =" 0"
144192 step =" 1"
145- class =" mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
193+ :class =" [
194+ 'mt-1 block w-full rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm',
195+ fieldHasError('list_order')
196+ ? 'border border-red-300 focus:ring-red-500 focus:border-red-500'
197+ : 'border border-slate-300 focus:ring-blue-500 focus:border-blue-500'
198+ ]"
199+ >
200+ <p
201+ v-for =" message in fieldErrors('list_order')"
202+ :key =" `list-order-${message}`"
203+ class =" mt-1 text-sm text-red-600"
146204 >
205+ {{ message }}
206+ </p >
147207 </div >
148208
149209 <p v-if =" createError" class =" text-sm text-red-600" >{{ createError }}</p >
@@ -180,6 +240,7 @@ const allBounceRules = ref([])
180240const isCreateModalOpen = ref (false )
181241const isCreatingRule = ref (false )
182242const createError = ref (' ' )
243+ const createFieldErrors = ref ({})
183244const createForm = ref ({
184245 regex: ' ' ,
185246 comment: ' ' ,
@@ -212,8 +273,48 @@ const resetCreateForm = () => {
212273 list_order: ' ' ,
213274 }
214275 createError .value = ' '
276+ createFieldErrors .value = {}
215277}
216278
279+ const normalizeValidationErrors = (error ) => {
280+ const responseData = error? .responseData
281+ if (! responseData || typeof responseData !== ' object' || Array .isArray (responseData)) {
282+ return {}
283+ }
284+
285+ const sourceErrors =
286+ responseData .errors && typeof responseData .errors === ' object' && ! Array .isArray (responseData .errors )
287+ ? responseData .errors
288+ : responseData
289+
290+ const normalized = {}
291+
292+ Object .entries (sourceErrors).forEach (([field , messages ]) => {
293+ if (! field || messages === null || messages === undefined ) {
294+ return
295+ }
296+
297+ const key = String (field)
298+ const list = Array .isArray (messages) ? messages : [messages]
299+ const textMessages = list
300+ .map ((message ) => String (message).trim ())
301+ .filter (Boolean )
302+
303+ if (textMessages .length > 0 ) {
304+ normalized[key] = textMessages
305+ }
306+ })
307+
308+ return normalized
309+ }
310+
311+ const fieldErrors = (field ) => {
312+ const messages = createFieldErrors .value ? .[field]
313+ return Array .isArray (messages) ? messages : []
314+ }
315+
316+ const fieldHasError = (field ) => fieldErrors (field).length > 0
317+
217318const loadBounceRules = async () => {
218319 try {
219320 const bounceRules = await bouncesClient .listRegex ()
@@ -247,7 +348,8 @@ const submitCreateRule = async () => {
247348
248349 const regex = createForm .value .regex .trim ()
249350 if (! regex) {
250- createError .value = ' Regex is required.'
351+ createFieldErrors .value = { regex: [' Regex is required.' ] }
352+ createError .value = ' '
251353 return
252354 }
253355
@@ -269,21 +371,29 @@ const submitCreateRule = async () => {
269371 if (createForm .value .list_order !== ' ' ) {
270372 const parsedListOrder = Number (createForm .value .list_order )
271373 if (! Number .isInteger (parsedListOrder) || parsedListOrder < 0 ) {
272- createError .value = ' List Order must be a whole number greater than or equal to 0.'
374+ createFieldErrors .value = {
375+ ... createFieldErrors .value ,
376+ list_order: [' List Order must be a whole number greater than or equal to 0.' ]
377+ }
378+ createError .value = ' '
273379 return
274380 }
275381 payload .list_order = parsedListOrder
276382 }
277383
278384 isCreatingRule .value = true
279385 createError .value = ' '
386+ createFieldErrors .value = {}
280387
281388 try {
282389 await bouncesClient .upsertRegex (payload)
283390 isCreateModalOpen .value = false
284391 await loadBounceRules ()
285392 } catch (error) {
286- createError .value = error? .message ?? ' Failed to create rule.'
393+ createFieldErrors .value = normalizeValidationErrors (error)
394+ createError .value = Object .keys (createFieldErrors .value ).length > 0
395+ ? ' '
396+ : error? .message ?? ' Failed to create rule.'
287397 } finally {
288398 isCreatingRule .value = false
289399 }
0 commit comments