Skip to content

Commit 22aa396

Browse files
committed
Create form errors
1 parent 7055faf commit 22aa396

1 file changed

Lines changed: 118 additions & 8 deletions

File tree

assets/vue/components/bounces/BounceRules.vue

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,20 @@
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>
@@ -105,8 +117,20 @@
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">
@@ -115,22 +139,46 @@
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

@@ -142,8 +190,20 @@
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([])
180240
const isCreateModalOpen = ref(false)
181241
const isCreatingRule = ref(false)
182242
const createError = ref('')
243+
const createFieldErrors = ref({})
183244
const 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+
217318
const 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

Comments
 (0)