@asd20/ui-next 2.23.4 → 2.23.6
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 +14 -0
- package/package.json +1 -1
- package/src/components/organisms/Asd20AccessibilityIssueModal/index.vue +187 -22
- package/src/components/organisms/Asd20ComposeEmailModal/index.vue +182 -23
- package/src/components/utils/Recaptcha.vue +1 -1
- package/src/helpers/sendAccessibilityIssue.js +42 -49
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.23.6](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.5...ui-next-v2.23.6) (2026-09-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* restore/harden accessibility issue form ([d7ef90c](https://github.com/academydistrict20/asd20-ui-next/commit/d7ef90c76f1675e53528374cf226d05d38af1929))
|
|
9
|
+
|
|
10
|
+
## [2.23.5](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.4...ui-next-v2.23.5) (2026-09-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
|
|
3
17
|
## [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
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<div
|
|
12
12
|
v-if="submissionSuccess"
|
|
13
13
|
class="submission-success"
|
|
14
|
+
role="status"
|
|
15
|
+
aria-live="polite"
|
|
14
16
|
>
|
|
15
17
|
<p style="text-align: center; font-weight: bold; font-size: 1.1rem;">
|
|
16
18
|
Thank you! Your issue has been submitted.
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
id="issue"
|
|
61
63
|
v-model="form.issue"
|
|
62
64
|
label="Describe the Issue"
|
|
65
|
+
maxlength="2000"
|
|
63
66
|
help-text="<ul style='margin: 0 0 0.5rem 1rem; font-size: 0.75rem !important;'>
|
|
64
67
|
<li>Specify the exact URL, program, service, or digital asset you had a problem with.</li>
|
|
65
68
|
<li>Provide as much detail as possible, including the nature of the accessibility barrier, the date and time it occurred, any assistive technology you were using, or the nature of the program or service being denied.</li>
|
|
@@ -69,18 +72,55 @@
|
|
|
69
72
|
:validator="validateIssue"
|
|
70
73
|
@validated="validationErrors.issue = $event"
|
|
71
74
|
/>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
<div
|
|
76
|
+
class="asd20-accessibility-issue-modal__honeypot"
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
>
|
|
79
|
+
<label for="accessibility-website">Website</label>
|
|
80
|
+
<input
|
|
81
|
+
id="accessibility-website"
|
|
82
|
+
v-model="form.website"
|
|
83
|
+
type="text"
|
|
84
|
+
name="website"
|
|
85
|
+
tabindex="-1"
|
|
86
|
+
autocomplete="off"
|
|
87
|
+
>
|
|
88
|
+
</div>
|
|
89
|
+
<div
|
|
90
|
+
class="asd20-accessibility-issue-modal__submit"
|
|
91
|
+
aria-live="polite"
|
|
92
|
+
>
|
|
93
|
+
<Recaptcha
|
|
94
|
+
v-if="!sending"
|
|
95
|
+
sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
|
|
96
|
+
:load-recaptcha-script="true"
|
|
97
|
+
size="invisible"
|
|
98
|
+
@verify="captchaVerified"
|
|
99
|
+
@error="captchaFailed"
|
|
100
|
+
>
|
|
101
|
+
<asd20-button
|
|
102
|
+
label="Submit Issue"
|
|
103
|
+
:disabled="!isValid || sending"
|
|
104
|
+
horizontal
|
|
105
|
+
centered
|
|
106
|
+
bordered
|
|
107
|
+
@click="validateBeforeSend"
|
|
108
|
+
/>
|
|
109
|
+
</Recaptcha>
|
|
110
|
+
<asd20-spinner
|
|
111
|
+
v-else
|
|
112
|
+
size="sm"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<div
|
|
116
|
+
v-if="feedbackMessage"
|
|
117
|
+
ref="feedback"
|
|
118
|
+
class="asd20-accessibility-issue-modal__feedback"
|
|
119
|
+
:role="feedbackRole"
|
|
120
|
+
tabindex="-1"
|
|
121
|
+
>
|
|
122
|
+
{{ feedbackMessage }}
|
|
123
|
+
</div>
|
|
84
124
|
</template>
|
|
85
125
|
</Asd20Viewport>
|
|
86
126
|
</asd20-modal>
|
|
@@ -93,8 +133,10 @@ import Asd20TextAreaInput from '../../molecules/Asd20TextAreaInput'
|
|
|
93
133
|
import Asd20Viewport from '../../atoms/Asd20Viewport'
|
|
94
134
|
import Asd20Spinner from '../../atoms/Asd20Spinner'
|
|
95
135
|
import Asd20Button from '../../atoms/Asd20Button'
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
import Recaptcha from '../../utils/Recaptcha'
|
|
137
|
+
import sendAccessibilityIssue, {
|
|
138
|
+
resolveAccessibilityIssueEndpoint,
|
|
139
|
+
} from '../../../helpers/sendAccessibilityIssue.js'
|
|
98
140
|
|
|
99
141
|
export default {
|
|
100
142
|
name: 'Asd20AccessibilityIssueModal',
|
|
@@ -105,6 +147,7 @@ export default {
|
|
|
105
147
|
Asd20Viewport,
|
|
106
148
|
Asd20Spinner,
|
|
107
149
|
Asd20Button,
|
|
150
|
+
Recaptcha,
|
|
108
151
|
},
|
|
109
152
|
props: {
|
|
110
153
|
open: { type: Boolean, default: false },
|
|
@@ -113,6 +156,8 @@ export default {
|
|
|
113
156
|
return {
|
|
114
157
|
sending: false,
|
|
115
158
|
submissionSuccess: false,
|
|
159
|
+
feedbackMessage: '',
|
|
160
|
+
feedbackRole: 'alert',
|
|
116
161
|
form: {
|
|
117
162
|
firstName: '',
|
|
118
163
|
lastName: '',
|
|
@@ -121,6 +166,7 @@ export default {
|
|
|
121
166
|
address: '',
|
|
122
167
|
organization: '',
|
|
123
168
|
issue: '',
|
|
169
|
+
website: '',
|
|
124
170
|
},
|
|
125
171
|
validationErrors: {
|
|
126
172
|
firstName: [],
|
|
@@ -135,13 +181,20 @@ export default {
|
|
|
135
181
|
return Object.values(this.validationErrors).every(e => e.length === 0)
|
|
136
182
|
},
|
|
137
183
|
},
|
|
184
|
+
watch: {
|
|
185
|
+
open(value) {
|
|
186
|
+
if (value) {
|
|
187
|
+
this.submissionSuccess = false
|
|
188
|
+
this.feedbackMessage = ''
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
},
|
|
138
192
|
methods: {
|
|
139
193
|
validateEmailAddress({ value, validationErrors }) {
|
|
140
194
|
const pattern = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
|
|
141
195
|
if (!pattern.test(value)) {
|
|
142
196
|
validationErrors.push('A valid email address is required')
|
|
143
197
|
}
|
|
144
|
-
console.log('Email validation errors:', validationErrors)
|
|
145
198
|
return validationErrors
|
|
146
199
|
},
|
|
147
200
|
validateIssue({ value, validationErrors }) {
|
|
@@ -155,25 +208,116 @@ export default {
|
|
|
155
208
|
}
|
|
156
209
|
return validationErrors
|
|
157
210
|
},
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
resolveRuntimeConfig() {
|
|
212
|
+
return (
|
|
213
|
+
this.$config ||
|
|
214
|
+
this.$?.appContext?.config?.globalProperties?.$config ||
|
|
215
|
+
{}
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
getFormValidationMessages() {
|
|
219
|
+
const messages = []
|
|
220
|
+
const emailPattern =
|
|
221
|
+
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
|
|
222
|
+
|
|
223
|
+
if (!String(this.form.firstName || '').trim())
|
|
224
|
+
messages.push('First name is required.')
|
|
225
|
+
if (!String(this.form.lastName || '').trim())
|
|
226
|
+
messages.push('Last name is required.')
|
|
227
|
+
if (!emailPattern.test(String(this.form.email || '').trim()))
|
|
228
|
+
messages.push('Enter a valid email address.')
|
|
229
|
+
|
|
230
|
+
const issue = String(this.form.issue || '').trim()
|
|
231
|
+
if (!issue) messages.push('Describe the accessibility issue.')
|
|
232
|
+
else if (issue.length > 2000)
|
|
233
|
+
messages.push('The issue description must be 2,000 characters or fewer.')
|
|
234
|
+
|
|
235
|
+
return messages
|
|
236
|
+
},
|
|
237
|
+
validateBeforeSend(event) {
|
|
238
|
+
const messages = this.getFormValidationMessages()
|
|
239
|
+
if (!messages.length) return
|
|
240
|
+
|
|
241
|
+
event?.preventDefault?.()
|
|
242
|
+
this.showFeedback(messages.join(' '))
|
|
243
|
+
},
|
|
244
|
+
captchaVerified(token) {
|
|
245
|
+
if (token) this.sendAccessibilityIssue(token)
|
|
246
|
+
},
|
|
247
|
+
captchaFailed() {
|
|
248
|
+
this.showFeedback(
|
|
249
|
+
'The verification service could not start. Please try again or use the email and phone options on this page.'
|
|
250
|
+
)
|
|
251
|
+
},
|
|
252
|
+
showFeedback(message, role = 'alert') {
|
|
253
|
+
this.feedbackMessage = message
|
|
254
|
+
this.feedbackRole = role
|
|
255
|
+
this.$nextTick(() => this.$refs.feedback?.focus?.())
|
|
256
|
+
},
|
|
257
|
+
getServerFailureMessage(error) {
|
|
258
|
+
const status = Number(error?.response?.status || 0)
|
|
259
|
+
const serverMessage = error?.response?.data?.error
|
|
260
|
+
|
|
261
|
+
if (status === 400 && serverMessage) return serverMessage
|
|
262
|
+
if (status === 403)
|
|
263
|
+
return 'The verification was not accepted. Please try again.'
|
|
264
|
+
if (status === 429)
|
|
265
|
+
return 'Too many reports were submitted recently. Please wait and try again, or use the email and phone options on this page.'
|
|
266
|
+
|
|
267
|
+
return 'Something went wrong while submitting the form. Please try again later or use the email and phone options on this page.'
|
|
268
|
+
},
|
|
269
|
+
async sendAccessibilityIssue(captchaToken = '') {
|
|
270
|
+
const validationMessages = this.getFormValidationMessages()
|
|
271
|
+
if (validationMessages.length) {
|
|
272
|
+
this.showFeedback(validationMessages.join(' '))
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const endpoint = resolveAccessibilityIssueEndpoint({
|
|
277
|
+
runtimeConfig: this.resolveRuntimeConfig(),
|
|
278
|
+
})
|
|
279
|
+
const submissionClient =
|
|
280
|
+
this.$sendAccessibilityIssue ||
|
|
281
|
+
(endpoint
|
|
282
|
+
? async issue => await sendAccessibilityIssue(issue, endpoint)
|
|
283
|
+
: null)
|
|
284
|
+
|
|
285
|
+
if (!submissionClient) {
|
|
286
|
+
console.error('Accessibility issue endpoint is not configured.')
|
|
287
|
+
this.showFeedback(
|
|
288
|
+
'Issue reporting is not configured for this environment. Please use the email and phone options on this page.'
|
|
289
|
+
)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
160
293
|
this.sending = true
|
|
294
|
+
this.feedbackMessage = ''
|
|
161
295
|
|
|
162
296
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
await submissionClient({
|
|
298
|
+
...this.form,
|
|
299
|
+
captchaToken,
|
|
300
|
+
})
|
|
165
301
|
this.submissionSuccess = true
|
|
166
302
|
|
|
167
303
|
// Wait 2 seconds, then close the modal
|
|
168
304
|
setTimeout(() => {
|
|
169
305
|
this.submissionSuccess = false
|
|
306
|
+
this.form = {
|
|
307
|
+
firstName: '',
|
|
308
|
+
lastName: '',
|
|
309
|
+
email: '',
|
|
310
|
+
phone: '',
|
|
311
|
+
address: '',
|
|
312
|
+
organization: '',
|
|
313
|
+
issue: '',
|
|
314
|
+
website: '',
|
|
315
|
+
}
|
|
170
316
|
this.$emit('dismiss')
|
|
171
317
|
}, 2000)
|
|
172
318
|
} catch (e) {
|
|
173
319
|
console.error('Accessibility issue submission failed:', e.message || e)
|
|
174
|
-
|
|
175
|
-
'Something went wrong while submitting the form. Please try again later.'
|
|
176
|
-
)
|
|
320
|
+
this.showFeedback(this.getServerFailureMessage(e))
|
|
177
321
|
} finally {
|
|
178
322
|
this.sending = false
|
|
179
323
|
}
|
|
@@ -193,6 +337,27 @@ export default {
|
|
|
193
337
|
font-family: Arial, sans-serif;
|
|
194
338
|
font-size: 1rem;
|
|
195
339
|
}
|
|
340
|
+
|
|
341
|
+
&__submit {
|
|
342
|
+
display: flex;
|
|
343
|
+
justify-content: center;
|
|
344
|
+
min-height: 3rem;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
&__feedback {
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
margin: space(0.5) 0;
|
|
350
|
+
text-align: center;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
&__honeypot {
|
|
354
|
+
position: absolute;
|
|
355
|
+
width: 1px;
|
|
356
|
+
height: 1px;
|
|
357
|
+
margin: -1px;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
clip: rect(0, 0, 0, 0);
|
|
360
|
+
}
|
|
196
361
|
}
|
|
197
362
|
|
|
198
363
|
@media (min-width: 1024px) {
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
'Email
|
|
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
|
-
|
|
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,71 +1,64 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
|
|
3
|
-
import { resolveRuntimeConfigValue } from './runtimeConfig'
|
|
3
|
+
import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
|
|
4
|
+
|
|
5
|
+
const ACCESSIBILITY_ISSUE_ROUTE = '/api/accessibility/issues'
|
|
4
6
|
|
|
5
7
|
function issueIsValid(issue) {
|
|
6
|
-
return
|
|
8
|
+
return Boolean(
|
|
9
|
+
issue &&
|
|
10
|
+
String(issue.firstName || '').trim() &&
|
|
11
|
+
String(issue.lastName || '').trim() &&
|
|
12
|
+
String(issue.email || '').trim() &&
|
|
13
|
+
String(issue.issue || '').trim()
|
|
14
|
+
)
|
|
7
15
|
}
|
|
8
16
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
17
|
+
export function resolveAccessibilityIssueEndpoint({
|
|
18
|
+
endpoint = null,
|
|
19
|
+
runtimeConfig = {},
|
|
20
|
+
} = {}) {
|
|
21
|
+
const explicitEndpoint = resolveRuntimeConfigValue({
|
|
22
|
+
explicitValue: endpoint,
|
|
23
|
+
runtimeConfig,
|
|
24
|
+
runtimeConfigKeys: ['accessibilityIssueEndpoint'],
|
|
25
|
+
envKeys: ['ACCESSIBILITY_ISSUE_ENDPOINT'],
|
|
26
|
+
})
|
|
27
|
+
if (explicitEndpoint) return String(explicitEndpoint).trim()
|
|
19
28
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
categoryId: options,
|
|
25
|
-
}
|
|
26
|
-
const categoryId = resolveRuntimeConfigValue({
|
|
27
|
-
explicitValue: resolvedOptions.categoryId,
|
|
28
|
-
runtimeConfig: resolvedOptions.runtimeConfig,
|
|
29
|
-
runtimeConfigKeys: ['happyFoxCategoryId'],
|
|
30
|
-
envKeys: ['HAPPYFOX_CATEGORY_ID'],
|
|
29
|
+
const functionsEndpoint = resolveRuntimeConfigValue({
|
|
30
|
+
runtimeConfig,
|
|
31
|
+
runtimeConfigKeys: ['functionsEndpoint'],
|
|
32
|
+
envKeys: ['FUNCTIONS_ENDPOINT'],
|
|
31
33
|
})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const baseUrl = normalizeBaseUrl(functionsEndpoint)
|
|
35
|
+
|
|
36
|
+
return baseUrl ? `${baseUrl}${ACCESSIBILITY_ISSUE_ROUTE}` : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function sendAccessibilityIssue(issue, endpoint) {
|
|
40
|
+
if (!issueIsValid(issue)) {
|
|
41
|
+
throw new Error('The issue object is incomplete or invalid.')
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
email: issue.email,
|
|
39
|
-
phone: issue.phone || '',
|
|
40
|
-
subject: 'Accessibility Issue Reported via External ASD20 Website',
|
|
41
|
-
html: `
|
|
42
|
-
<p><strong>Issue Details:</strong></p>
|
|
43
|
-
<p>${issue.issue}</p>
|
|
44
|
-
<p><strong>Mailing Address:</strong> ${issue.address || 'Not provided'}</p>
|
|
45
|
-
<p><strong>Organization:</strong> ${issue.organization || 'Not provided'}</p>
|
|
46
|
-
`,
|
|
47
|
-
category: Number(categoryId),
|
|
44
|
+
if (!endpoint) {
|
|
45
|
+
throw new Error('The accessibility issue endpoint is not configured.')
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
try {
|
|
51
|
-
const response = await axios.post(endpoint,
|
|
52
|
-
headers: {
|
|
53
|
-
Authorization: `Basic ${btoa(`${apiKey}:${authCode}`)}`,
|
|
54
|
-
'Content-Type': 'application/json',
|
|
55
|
-
Accept: 'application/json'
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
49
|
+
const response = await axios.post(endpoint, issue)
|
|
59
50
|
return response.data
|
|
60
51
|
} catch (error) {
|
|
61
52
|
if (error.response) {
|
|
62
|
-
|
|
63
|
-
`
|
|
53
|
+
const submissionError = new Error(
|
|
54
|
+
`Accessibility submission error ${error.response.status}: ${JSON.stringify(
|
|
64
55
|
error.response.data
|
|
65
56
|
)}`
|
|
66
57
|
)
|
|
67
|
-
|
|
68
|
-
throw
|
|
58
|
+
submissionError.response = error.response
|
|
59
|
+
throw submissionError
|
|
69
60
|
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`Accessibility submission request error: ${error.message}`)
|
|
70
63
|
}
|
|
71
64
|
}
|