@asd20/ui-next 2.23.4 → 2.23.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.23.5](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.4...ui-next-v2.23.5) (2026-09-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * explain directory email validation errors ([#1](https://github.com/academydistrict20/asd20-ui-next/issues/1)) ([070c408](https://github.com/academydistrict20/asd20-ui-next/commit/070c40854c1ec4f8790abfc4077da9c0aa7b44e6))
9
+
3
10
  ## [2.23.4](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.3...ui-next-v2.23.4) (2026-09-10)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.23.4",
3
+ "version": "2.23.5",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  <asd20-text-input
13
13
  id="senderName"
14
14
  v-model="emailMessage.senderName"
15
+ :validator="validateSenderName"
15
16
  label="Your Full Name"
16
17
  required
17
18
  @validated="validationErrors.senderName = $event"
@@ -28,6 +29,7 @@
28
29
  <asd20-text-input
29
30
  id="subject"
30
31
  v-model="emailMessage.subject"
32
+ :validator="validateSubject"
31
33
  label="Subject"
32
34
  maxlength="200"
33
35
  required
@@ -36,8 +38,10 @@
36
38
  <asd20-text-area-input
37
39
  id="messageBody"
38
40
  v-model="emailMessage.messageBody"
41
+ :validator="validateMessageBody"
39
42
  label="Message"
40
43
  maxlength="8000"
44
+ help-text="Please enter at least 5 characters."
41
45
  required
42
46
  @validated="validationErrors.messageBody = $event"
43
47
  />
@@ -46,18 +50,18 @@
46
50
  aria-live="polite"
47
51
  >
48
52
  <Recaptcha
49
- v-if="!sending && !sendSucceeded && !sendRejected"
53
+ v-if="!sending && !sendSucceeded"
50
54
  sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
51
55
  :load-recaptcha-script="true"
52
56
  size="invisible"
53
57
  @verify="captchaVerified"
54
58
  >
55
59
  <asd20-button
56
- :disabled="!isValid"
57
60
  label="Send"
58
61
  horizontal
59
62
  centered
60
63
  bordered
64
+ @click="validateBeforeSend"
61
65
  />
62
66
  </Recaptcha>
63
67
  <asd20-spinner
@@ -73,6 +77,40 @@
73
77
  </p>
74
78
  </div>
75
79
  </Asd20Viewport>
80
+
81
+ <asd20-modal
82
+ :open="feedbackModalOpen"
83
+ :title="feedbackTitle"
84
+ icon="alert"
85
+ dismissable
86
+ windowed
87
+ class="asd20-compose-email-modal__feedback-modal"
88
+ @dismiss="dismissFeedbackModal"
89
+ >
90
+ <div
91
+ class="asd20-compose-email-modal__feedback"
92
+ role="alert"
93
+ >
94
+ <p>{{ feedbackIntroduction }}</p>
95
+ <ul v-if="feedbackMessages.length">
96
+ <li
97
+ v-for="message in feedbackMessages"
98
+ :key="message"
99
+ >
100
+ {{ message }}
101
+ </li>
102
+ </ul>
103
+ </div>
104
+ <template #footer>
105
+ <asd20-button
106
+ label="Return to form"
107
+ horizontal
108
+ centered
109
+ bordered
110
+ @click="dismissFeedbackModal"
111
+ />
112
+ </template>
113
+ </asd20-modal>
76
114
  </asd20-modal>
77
115
  </template>
78
116
 
@@ -111,7 +149,10 @@ export default {
111
149
  composing: false,
112
150
  sending: false,
113
151
  sendSucceeded: false,
114
- sendRejected: false,
152
+ feedbackModalOpen: false,
153
+ feedbackTitle: '',
154
+ feedbackIntroduction: '',
155
+ feedbackMessages: [],
115
156
  emailMessage: {
116
157
  senderName: '',
117
158
  senderEmail: '',
@@ -124,16 +165,11 @@ export default {
124
165
  return 'Send an Email'
125
166
  },
126
167
  sendStatusMessage() {
127
- return this.sendRejected
128
- ? "Your message couldn't be sent via this form."
129
- : 'Your message was sent.'
168
+ return 'Your message was sent.'
130
169
  },
131
170
  isValid() {
132
171
  return (
133
- Boolean(this.emailMessage.senderName) &&
134
- Boolean(this.emailMessage.senderEmail) &&
135
- Boolean(this.emailMessage.subject) &&
136
- Boolean(this.emailMessage.messageBody) &&
172
+ this.getFormValidationMessages().length === 0 &&
137
173
  this.validationErrors.senderName.length === 0 &&
138
174
  this.validationErrors.senderEmail.length === 0 &&
139
175
  this.validationErrors.subject.length === 0 &&
@@ -176,10 +212,57 @@ export default {
176
212
  const pattern =
177
213
  /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
178
214
  const regex = RegExp(pattern)
179
- if (!regex.test(value))
215
+ if (!regex.test(String(value || '').trim()))
180
216
  validationErrors.push('A valid email address is required')
181
217
  return validationErrors
182
218
  },
219
+ validateSenderName({ value, validationErrors }) {
220
+ if (!String(value || '').trim())
221
+ validationErrors.push('Your full name is required.')
222
+ return validationErrors
223
+ },
224
+ validateSubject({ value, validationErrors }) {
225
+ if (!String(value || '').trim())
226
+ validationErrors.push('A subject is required.')
227
+ return validationErrors
228
+ },
229
+ validateMessageBody({ value, validationErrors }) {
230
+ const message = String(value || '').trim()
231
+ if (message && message.length < 5)
232
+ validationErrors.push('The message must be at least 5 characters.')
233
+ return validationErrors
234
+ },
235
+ getFormValidationMessages() {
236
+ const messages = []
237
+ const senderName = String(this.emailMessage.senderName || '').trim()
238
+ const senderEmail = String(this.emailMessage.senderEmail || '').trim()
239
+ const subject = String(this.emailMessage.subject || '').trim()
240
+ const messageBody = String(this.emailMessage.messageBody || '').trim()
241
+ const emailPattern =
242
+ /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
243
+
244
+ if (!senderName) messages.push('Your full name is required.')
245
+ if (!senderEmail) messages.push('Your email address is required.')
246
+ else if (!emailPattern.test(senderEmail))
247
+ messages.push('Enter a valid email address.')
248
+ if (!subject) messages.push('A subject is required.')
249
+ if (!messageBody) messages.push('A message is required.')
250
+ else if (messageBody.length < 5)
251
+ messages.push('The message must be at least 5 characters.')
252
+
253
+ return messages
254
+ },
255
+ validateBeforeSend(event) {
256
+ const messages = this.getFormValidationMessages()
257
+ if (!messages.length) return
258
+
259
+ event?.preventDefault?.()
260
+ this.showFeedback(
261
+ 'Please check your message',
262
+ 'Correct the following fields before sending:',
263
+ messages
264
+ )
265
+ },
183
266
  composeEmail() {
184
267
  if (!this.recipientId) return
185
268
  this.emailMessage = {
@@ -198,7 +281,16 @@ export default {
198
281
  resetSendState() {
199
282
  this.sending = false
200
283
  this.sendSucceeded = false
201
- this.sendRejected = false
284
+ this.dismissFeedbackModal()
285
+ },
286
+ dismissFeedbackModal() {
287
+ this.feedbackModalOpen = false
288
+ },
289
+ showFeedback(title, introduction, messages = []) {
290
+ this.feedbackTitle = title
291
+ this.feedbackIntroduction = introduction
292
+ this.feedbackMessages = messages
293
+ this.feedbackModalOpen = true
202
294
  },
203
295
  waitForSuccessMessage() {
204
296
  return new Promise(resolve => {
@@ -237,8 +329,64 @@ export default {
237
329
  this.getSendEmailResponseData(error)?.code === 'moderation_rejected'
238
330
  )
239
331
  },
332
+ getServerValidationMessages(error) {
333
+ if (this.getSendEmailErrorStatus(error) !== 400) return []
334
+
335
+ const errorMessage = this.getSendEmailResponseData(error)?.error
336
+ const messages = {
337
+ 'senderName is required.': 'Your full name is required.',
338
+ 'senderEmail must be a valid email address.':
339
+ 'Enter a valid email address.',
340
+ 'messageBody is required.': 'A message is required.',
341
+ 'messageBody is too short.':
342
+ 'The message must be at least 5 characters.',
343
+ 'messageBody is too long.':
344
+ 'The message must be 8,000 characters or fewer.',
345
+ 'Recipient was not found or does not have a public email address.':
346
+ 'The selected recipient is no longer available for public email.',
347
+ }
348
+
349
+ return [
350
+ messages[errorMessage] ||
351
+ 'Some information was not accepted. Review each field and try again.',
352
+ ]
353
+ },
354
+ showSendFailure(error) {
355
+ const validationMessages = this.getServerValidationMessages(error)
356
+ if (validationMessages.length) {
357
+ this.showFeedback(
358
+ 'Please check your message',
359
+ 'Correct the following fields before sending:',
360
+ validationMessages
361
+ )
362
+ return
363
+ }
364
+
365
+ if (this.isModerationRejection(error)) {
366
+ this.showFeedback(
367
+ 'Message not sent',
368
+ "Your message couldn't be sent through this form.",
369
+ []
370
+ )
371
+ return
372
+ }
373
+
374
+ this.showFeedback(
375
+ 'Email not sent',
376
+ this.getSendEmailFailureMessage(error),
377
+ []
378
+ )
379
+ },
240
380
  async sendEmail(captchaToken = '') {
241
- if (!this.isValid) return
381
+ const validationMessages = this.getFormValidationMessages()
382
+ if (validationMessages.length) {
383
+ this.showFeedback(
384
+ 'Please check your message',
385
+ 'Correct the following fields before sending:',
386
+ validationMessages
387
+ )
388
+ return
389
+ }
242
390
 
243
391
  const endpoint = this.resolveSendEmailEndpoint()
244
392
  const sendEmailClient =
@@ -247,8 +395,10 @@ export default {
247
395
 
248
396
  if (!sendEmailClient) {
249
397
  console.error('Send email endpoint is not configured.')
250
- alert(
251
- 'Email sending is not configured for this environment. Please try again later.'
398
+ this.showFeedback(
399
+ 'Email not sent',
400
+ 'Email sending is not configured for this environment. Please try again later.',
401
+ []
252
402
  )
253
403
  return
254
404
  }
@@ -269,14 +419,7 @@ export default {
269
419
  } catch (error) {
270
420
  this.sendSucceeded = false
271
421
  console.error('Email send failed:', error?.message || error)
272
- if (this.isModerationRejection(error)) {
273
- this.sending = false
274
- this.sendRejected = true
275
- await this.waitForSuccessMessage()
276
- this.$emit('dismiss')
277
- return
278
- }
279
- alert(this.getSendEmailFailureMessage(error))
422
+ this.showSendFailure(error)
280
423
  } finally {
281
424
  this.sending = false
282
425
  }
@@ -305,6 +448,22 @@ export default {
305
448
  margin: 0;
306
449
  text-align: center;
307
450
  }
451
+
452
+ &__feedback {
453
+ padding: space(1);
454
+
455
+ p {
456
+ margin-top: 0;
457
+ }
458
+
459
+ ul {
460
+ margin-bottom: 0;
461
+ }
462
+ }
463
+
464
+ &__feedback-modal :deep(.asd20-modal__footer) {
465
+ padding: space(0.5);
466
+ }
308
467
  }
309
468
 
310
469
  @media (min-width: 1024px) {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="asd20-recaptcha"
4
- @click.capture="handleClick"
4
+ @click="handleClick"
5
5
  >
6
6
  <slot />
7
7
  <div