@asd20/ui-next 2.31.7 → 2.32.0
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/Asd20ComposeEmailModal/index.vue +33 -9
- package/src/components/templates/Asd20HandbookDocument/index.vue +18 -3
- package/src/components/templates/Asd20HandbookTemplate/index.vue +11 -1
- package/src/helpers/handbookPresentation.js +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.32.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.8...ui-next-v2.32.0) (2026-10-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* share handbook translations with multilingual PDF downloads ([a034f23](https://github.com/academydistrict20/asd20-ui-next/commit/a034f237a2021e92376a0ff511f1380286221266))
|
|
9
|
+
|
|
10
|
+
## [2.31.8](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.7...ui-next-v2.31.8) (2026-10-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* refine 'send an email' functionality - help text, failure messaging, rate limits, spam detection ([a9c4a28](https://github.com/academydistrict20/asd20-ui-next/commit/a9c4a28f1fdab4c0c551aa2cfb6808fbc29742d2))
|
|
16
|
+
|
|
3
17
|
## [2.31.7](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.6...ui-next-v2.31.7) (2026-10-12)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
:validator="validateMessageBody"
|
|
42
42
|
label="Message"
|
|
43
43
|
maxlength="8000"
|
|
44
|
-
help-text="
|
|
44
|
+
:help-text="messageHelpText"
|
|
45
45
|
required
|
|
46
46
|
@validated="validationErrors.messageBody = $event"
|
|
47
47
|
/>
|
|
@@ -124,6 +124,8 @@ import Asd20Button from '../../atoms/Asd20Button'
|
|
|
124
124
|
import Recaptcha from '../../utils/Recaptcha'
|
|
125
125
|
import sendEmail from '../../../helpers/sendEmail.js'
|
|
126
126
|
|
|
127
|
+
const MIN_MESSAGE_LENGTH = 10
|
|
128
|
+
|
|
127
129
|
export default {
|
|
128
130
|
name: 'Asd20ComposeEmailModal',
|
|
129
131
|
components: {
|
|
@@ -161,6 +163,11 @@ export default {
|
|
|
161
163
|
},
|
|
162
164
|
}),
|
|
163
165
|
computed: {
|
|
166
|
+
messageHelpText() {
|
|
167
|
+
return String(this.emailMessage.messageBody || '').trim().length < MIN_MESSAGE_LENGTH
|
|
168
|
+
? 'Please provide more detail.'
|
|
169
|
+
: ''
|
|
170
|
+
},
|
|
164
171
|
modalTitle() {
|
|
165
172
|
return 'Send an Email'
|
|
166
173
|
},
|
|
@@ -228,8 +235,8 @@ export default {
|
|
|
228
235
|
},
|
|
229
236
|
validateMessageBody({ value, validationErrors }) {
|
|
230
237
|
const message = String(value || '').trim()
|
|
231
|
-
if (message && message.length <
|
|
232
|
-
validationErrors.push('
|
|
238
|
+
if (message && message.length < MIN_MESSAGE_LENGTH)
|
|
239
|
+
validationErrors.push('Please provide more detail.')
|
|
233
240
|
return validationErrors
|
|
234
241
|
},
|
|
235
242
|
getFormValidationMessages() {
|
|
@@ -247,8 +254,8 @@ export default {
|
|
|
247
254
|
messages.push('Enter a valid email address.')
|
|
248
255
|
if (!subject) messages.push('A subject is required.')
|
|
249
256
|
if (!messageBody) messages.push('A message is required.')
|
|
250
|
-
else if (messageBody.length <
|
|
251
|
-
messages.push('
|
|
257
|
+
else if (messageBody.length < MIN_MESSAGE_LENGTH)
|
|
258
|
+
messages.push('Please provide more detail.')
|
|
252
259
|
|
|
253
260
|
return messages
|
|
254
261
|
},
|
|
@@ -304,11 +311,25 @@ export default {
|
|
|
304
311
|
const statusMatch = String(error?.message || '').match(/\b(\d{3})\b/)
|
|
305
312
|
return statusMatch ? Number(statusMatch[1]) : null
|
|
306
313
|
},
|
|
314
|
+
isDeliveryUnconfirmed(error) {
|
|
315
|
+
return this.getSendEmailResponseData(error)?.code === 'delivery_unconfirmed' || this.getSendEmailErrorStatus(error) === null
|
|
316
|
+
},
|
|
307
317
|
getSendEmailFailureMessage(error) {
|
|
318
|
+
if (this.isDeliveryUnconfirmed(error)) {
|
|
319
|
+
return "We couldn't confirm that your message was sent. Please check with the school before sending it again."
|
|
320
|
+
}
|
|
308
321
|
if (this.getSendEmailErrorStatus(error) === 429) {
|
|
322
|
+
const response = this.getSendEmailResponseData(error)
|
|
323
|
+
if (response?.code === 'rate_limited' && typeof response.error === 'string' && response.error.trim()) {
|
|
324
|
+
return response.error
|
|
325
|
+
}
|
|
309
326
|
return 'We could not send your message right now. Please wait and try again later.'
|
|
310
327
|
}
|
|
311
328
|
|
|
329
|
+
const response = this.getSendEmailResponseData(error)
|
|
330
|
+
if (['captcha_failed', 'email_unavailable'].includes(response?.code) && typeof response.error === 'string' && response.error.trim()) {
|
|
331
|
+
return response.error
|
|
332
|
+
}
|
|
312
333
|
return 'Something went wrong while sending your email. Please try again later.'
|
|
313
334
|
},
|
|
314
335
|
getSendEmailResponseData(error) {
|
|
@@ -341,7 +362,7 @@ export default {
|
|
|
341
362
|
'Enter a valid email address.',
|
|
342
363
|
'messageBody is required.': 'A message is required.',
|
|
343
364
|
'messageBody is too short.':
|
|
344
|
-
'
|
|
365
|
+
'Please provide more detail.',
|
|
345
366
|
'messageBody is too long.':
|
|
346
367
|
'The message must be 8,000 characters or fewer.',
|
|
347
368
|
'Recipient was not found or does not have a public email address.':
|
|
@@ -374,15 +395,18 @@ export default {
|
|
|
374
395
|
response?.code === 'moderation_unavailable'
|
|
375
396
|
? 'Email temporarily unavailable'
|
|
376
397
|
: 'Message not sent',
|
|
377
|
-
response?.error
|
|
378
|
-
|
|
398
|
+
typeof response?.error === 'string' && response.error.trim()
|
|
399
|
+
? response.error
|
|
400
|
+
: response?.code === 'moderation_unavailable'
|
|
401
|
+
? "We couldn't send your message right now. Please try again later. If the problem persists, please contact the Academy District 20 Help Desk at help@asd20.org or 719-234-1777."
|
|
402
|
+
: 'Your message could not be sent through this form.',
|
|
379
403
|
reference
|
|
380
404
|
)
|
|
381
405
|
return
|
|
382
406
|
}
|
|
383
407
|
|
|
384
408
|
this.showFeedback(
|
|
385
|
-
'Email not sent',
|
|
409
|
+
this.isDeliveryUnconfirmed(error) ? 'Delivery not confirmed' : 'Email not sent',
|
|
386
410
|
this.getSendEmailFailureMessage(error),
|
|
387
411
|
[]
|
|
388
412
|
)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<div
|
|
3
3
|
class="asd20-handbook-document"
|
|
4
4
|
:data-asd20-document-root="documentId"
|
|
5
|
+
:lang="languageCode"
|
|
6
|
+
:dir="languageDirection"
|
|
5
7
|
>
|
|
6
8
|
<header class="asd20-handbook-document__cover">
|
|
7
9
|
<div class="asd20-handbook-document__cover-heading">
|
|
@@ -13,10 +15,17 @@
|
|
|
13
15
|
v-if="organization.logoImageUrl"
|
|
14
16
|
class="asd20-handbook-document__logo"
|
|
15
17
|
:src="organization.logoImageUrl"
|
|
16
|
-
:alt="`${coverTitle.organizationTitle} logo`"
|
|
18
|
+
:alt="`${coverTitle.organizationTitle} ${documentLabels.logo || 'logo'}`"
|
|
17
19
|
/>
|
|
18
20
|
</div>
|
|
19
21
|
<div class="asd20-handbook-document__cover-body">
|
|
22
|
+
<p
|
|
23
|
+
v-if="languageCode !== 'en'"
|
|
24
|
+
class="asd20-handbook-document__translation-notice"
|
|
25
|
+
>
|
|
26
|
+
{{ documentLabels.automatedTranslation }}
|
|
27
|
+
{{ documentLabels.onlineAuthority }}
|
|
28
|
+
</p>
|
|
20
29
|
<asd20-messaging
|
|
21
30
|
:body-content="messages[0]?.bodyContent || ''"
|
|
22
31
|
:body-json="messages[0]?.bodyJson || ''"
|
|
@@ -24,7 +33,10 @@
|
|
|
24
33
|
/>
|
|
25
34
|
</div>
|
|
26
35
|
</header>
|
|
27
|
-
<asd20-print-contents
|
|
36
|
+
<asd20-print-contents
|
|
37
|
+
:headings="document.headings"
|
|
38
|
+
:label="documentLabels.contents || 'Contents'"
|
|
39
|
+
/>
|
|
28
40
|
<main>
|
|
29
41
|
<article
|
|
30
42
|
v-for="entry in document.entries"
|
|
@@ -75,11 +87,14 @@ export default {
|
|
|
75
87
|
printMetadata: { type: Object, default: () => ({}) },
|
|
76
88
|
includeIntroductionBody: { type: Boolean, default: true },
|
|
77
89
|
schoolYear: { type: String, default: '' },
|
|
90
|
+
languageCode: { type: String, default: 'en' },
|
|
91
|
+
languageDirection: { type: String, default: 'ltr' },
|
|
92
|
+
documentLabels: { type: Object, default: () => ({}) },
|
|
78
93
|
},
|
|
79
94
|
computed: {
|
|
80
95
|
document() { return buildHandbookDocument(this.$props) },
|
|
81
96
|
coverTitle() {
|
|
82
|
-
return handbookCoverTitle(this.organization, this.document.introduction.heading || this.title, this.printMetadata.generatedAt || new Date().toISOString())
|
|
97
|
+
return handbookCoverTitle(this.organization, this.document.introduction.heading || this.title, this.printMetadata.generatedAt || new Date().toISOString(), this.documentLabels)
|
|
83
98
|
},
|
|
84
99
|
},
|
|
85
100
|
}
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
:organization-options="organizationOptions"
|
|
35
35
|
:breadcrumb-links="breadcrumbLinks"
|
|
36
36
|
:language-code="languageCode"
|
|
37
|
+
:lang="contentLanguageCode || languageCode || 'en'"
|
|
38
|
+
:dir="contentDirection"
|
|
37
39
|
@update:languageCode="$emit('update:languageCode', $event)"
|
|
38
40
|
>
|
|
39
41
|
<template #calls-to-action="{ size }">
|
|
@@ -95,7 +97,10 @@
|
|
|
95
97
|
<p>
|
|
96
98
|
Our handbooks are presented in English. You can use the translate button at the top of the page to view the handbook in your preferred language.
|
|
97
99
|
</p>
|
|
98
|
-
<p>
|
|
100
|
+
<p v-if="translatedPdfAvailable">
|
|
101
|
+
When the handbook has finished translating, Download PDF provides a version in that language.
|
|
102
|
+
</p>
|
|
103
|
+
<p v-else>
|
|
99
104
|
<strong>Coming Soon:</strong> if you are viewing the handbook in a language other than English, and click Download PDF, you'll get a handbook version in that selected language.
|
|
100
105
|
</p>
|
|
101
106
|
<p>
|
|
@@ -123,6 +128,8 @@
|
|
|
123
128
|
|
|
124
129
|
<main
|
|
125
130
|
id="handbook-content"
|
|
131
|
+
:lang="contentLanguageCode || languageCode || 'en'"
|
|
132
|
+
:dir="contentDirection"
|
|
126
133
|
class="asd20-template-handbook__layout"
|
|
127
134
|
tabindex="-1"
|
|
128
135
|
>
|
|
@@ -299,6 +306,9 @@ export default {
|
|
|
299
306
|
},
|
|
300
307
|
props: {
|
|
301
308
|
documentPrint: { type: Boolean, default: false },
|
|
309
|
+
contentLanguageCode: { type: String, default: '' },
|
|
310
|
+
contentDirection: { type: String, default: 'ltr' },
|
|
311
|
+
translatedPdfAvailable: { type: Boolean, default: false },
|
|
302
312
|
printMetadata: { type: Object, default: () => ({}) },
|
|
303
313
|
publicationDocumentId: { type: String, default: 'handbook' },
|
|
304
314
|
publicationDocumentUrl: { type: String, default: '' },
|
|
@@ -30,35 +30,38 @@ export function isDistrictHandbook(organization = {}) {
|
|
|
30
30
|
)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function handbookTitles(organization, firstMessageTitle) {
|
|
33
|
+
function handbookTitles(organization, firstMessageTitle, labels = {}) {
|
|
34
34
|
const organizationTitle = isDistrictHandbook(organization)
|
|
35
35
|
? 'Academy District 20'
|
|
36
36
|
: organization.title || organization.name || 'School'
|
|
37
37
|
const handbookTitle = isDistrictHandbook(organization)
|
|
38
|
-
? firstMessageTitle || 'School Handbook'
|
|
39
|
-
: 'School Handbook'
|
|
38
|
+
? firstMessageTitle || labels.schoolHandbook || 'School Handbook'
|
|
39
|
+
: labels.schoolHandbook || 'School Handbook'
|
|
40
40
|
return { organizationTitle, handbookTitle }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function handbookCoverTitle(
|
|
44
44
|
organization = {},
|
|
45
45
|
firstMessageTitle = '',
|
|
46
|
-
generatedAt
|
|
46
|
+
generatedAt,
|
|
47
|
+
labels = {}
|
|
47
48
|
) {
|
|
48
|
-
const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle)
|
|
49
|
+
const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle, labels)
|
|
49
50
|
return { organizationTitle, handbookTitle: `${handbookSchoolYear(generatedAt)} ${handbookTitle}` }
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function handbookFooterTitle(organization = {}, firstMessageTitle = '') {
|
|
53
|
-
const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle)
|
|
53
|
+
export function handbookFooterTitle(organization = {}, firstMessageTitle = '', labels = {}) {
|
|
54
|
+
const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle, labels)
|
|
54
55
|
return `${organizationTitle} ${handbookTitle}`
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function handbookDownloadFilename(
|
|
58
59
|
organization = {},
|
|
59
60
|
generatedAt,
|
|
60
|
-
firstMessageTitle = ''
|
|
61
|
+
firstMessageTitle = '',
|
|
62
|
+
language = 'en'
|
|
61
63
|
) {
|
|
64
|
+
if (!/^[a-z]{2,3}(?:-[A-Za-z]{2,8})?$/.test(language)) throw new Error('Invalid handbook language.')
|
|
62
65
|
const safe = (value) =>
|
|
63
66
|
String(value || '')
|
|
64
67
|
.replace(/[^a-zA-Z0-9_. ()-]/g, '')
|
|
@@ -69,5 +72,5 @@ export function handbookDownloadFilename(
|
|
|
69
72
|
const title = isDistrictHandbook(organization)
|
|
70
73
|
? `ASD20 ${safe(firstMessageTitle) || 'School Handbook'}`
|
|
71
74
|
: `${abbreviation ? abbreviation + ' ' : ''}School Handbook`
|
|
72
|
-
return `${title} - ${year}-${month}-${day}.pdf`
|
|
75
|
+
return `${title}${language === 'en' ? '' : ` (${language})`} - ${year}-${month}-${day}.pdf`
|
|
73
76
|
}
|