@asd20/ui-next 2.35.2 → 2.35.3

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.35.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.35.2...ui-next-v2.35.3) (2026-11-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * give transcript requests a prominent alternate path in the student records form ([1250f67](https://github.com/academydistrict20/asd20-ui-next/commit/1250f6773bfa0047a234ea064463408cf4e51713))
9
+
3
10
  ## [2.35.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.35.1...ui-next-v2.35.2) (2026-11-04)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.35.2",
3
+ "version": "2.35.3",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -49,8 +49,37 @@
49
49
  novalidate
50
50
  @submit.prevent="validateBeforeCaptcha"
51
51
  >
52
+ <asd20-public-form-field
53
+ v-if="requestTypeField"
54
+ class="asd20-public-form-modal__requester-choice asd20-public-form-modal__records-choice"
55
+ :field="requestTypeField"
56
+ :input-id="fieldId('requesterType')"
57
+ :model-value="values.requesterType"
58
+ :errors="validationErrors.requesterType || []"
59
+ :show-pristine-validation-errors="false"
60
+ @update:modelValue="updateValue('requesterType', $event)"
61
+ />
62
+
63
+ <section v-if="isStudentRecordsForm && values.requesterType === 'transcriptOnly'">
64
+ <h3>Request a Transcript</h3>
65
+ <p>
66
+ For transcripts, graduation verification or replacement diplomas,
67
+ please use our dedicated Transcripts and Diplomas page.
68
+ </p>
69
+ <p>
70
+ <a :href="transcriptsHref">Go to Transcripts and Diplomas</a>
71
+ </p>
72
+ </section>
73
+
74
+ <template v-if="isStudentRecordsForm && showRequestFields">
75
+ <h3>{{ recordsRequestTitle }}</h3>
76
+ <p v-if="requestTypeDescription">
77
+ {{ requestTypeDescription }}
78
+ </p>
79
+ </template>
80
+
52
81
  <p
53
- v-if="formDefinition.introduction"
82
+ v-if="showRequestFields && formDefinition.introduction"
54
83
  class="asd20-public-form-modal__introduction"
55
84
  >
56
85
  <template v-if="inlineIntroductionParts">
@@ -65,7 +94,7 @@
65
94
  </p>
66
95
  <p
67
96
  v-if="
68
- formDefinition.introductionLink &&
97
+ showRequestFields && formDefinition.introductionLink &&
69
98
  !formDefinition.introductionLink.inline
70
99
  "
71
100
  >
@@ -128,6 +157,7 @@
128
157
  </div>
129
158
 
130
159
  <div
160
+ v-if="showRequestFields"
131
161
  class="asd20-public-form-modal__submit"
132
162
  aria-live="polite"
133
163
  >
@@ -197,6 +227,29 @@
197
227
  </div>
198
228
  </form>
199
229
  </asd20-viewport>
230
+
231
+ <asd20-modal
232
+ :open="open && showTranscriptReminder"
233
+ title="Transcript-only request"
234
+ dismissable
235
+ @dismiss="showTranscriptReminder = false"
236
+ >
237
+ <asd20-viewport scrollable>
238
+ <p>
239
+ You selected only a transcript. Most transcript-only requests should be
240
+ submitted through our Transcripts and Diplomas page.
241
+ </p>
242
+ <p>
243
+ <a :href="transcriptsHref">Go to Transcripts and Diplomas</a>
244
+ </p>
245
+ <asd20-button
246
+ label="Back to request"
247
+ horizontal
248
+ bordered
249
+ @click="showTranscriptReminder = false"
250
+ />
251
+ </asd20-viewport>
252
+ </asd20-modal>
200
253
  </asd20-modal>
201
254
  </template>
202
255
 
@@ -221,6 +274,11 @@ import {
221
274
  submitPublicForm,
222
275
  } from '../../../helpers/publicForms'
223
276
  import createComponentInstanceId from '../../../utils/createComponentInstanceId'
277
+ import {
278
+ studentRecordsPresentation,
279
+ studentRecordsSubmissionValues,
280
+ usesStudentRecordsRouting,
281
+ } from '../../../helpers/studentRecordsRequest'
224
282
 
225
283
  const DEFAULT_CAPTCHA_SITEKEY = '6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX'
226
284
 
@@ -253,6 +311,9 @@ export default {
253
311
  instanceId,
254
312
  honeypotId: `${instanceId}-website`,
255
313
  formDefinition: null,
314
+ serviceDefinition: null,
315
+ transcriptsHref: 'https://www.asd20.org/transcripts-and-diplomas/',
316
+ showTranscriptReminder: false,
256
317
  values: {},
257
318
  honeypot: '',
258
319
  validationErrors: {},
@@ -271,12 +332,15 @@ export default {
271
332
  canSubmit() {
272
333
  return Boolean(
273
334
  this.formDefinition &&
335
+ this.showRequestFields &&
274
336
  !this.sending &&
275
337
  validatePublicFormValues(this.formDefinition, this.values).valid
276
338
  )
277
339
  },
278
340
  visibleFields() {
279
341
  return (this.formDefinition?.fields || [])
342
+ .filter(field => !this.isStudentRecordsForm ||
343
+ (this.showRequestFields && field.name !== 'requesterType'))
280
344
  .filter(field => isPublicFormFieldVisible(field, this.values))
281
345
  .map(field => ({
282
346
  ...field,
@@ -286,6 +350,27 @@ export default {
286
350
  modalTitle() {
287
351
  return this.formDefinition?.title || 'Public Form'
288
352
  },
353
+ isStudentRecordsForm() {
354
+ return usesStudentRecordsRouting(this.formDefinition)
355
+ },
356
+ requestTypeField() {
357
+ return this.isStudentRecordsForm
358
+ ? this.formDefinition.fields.find(field => field.name === 'requesterType')
359
+ : null
360
+ },
361
+ showRequestFields() {
362
+ return !this.isStudentRecordsForm ||
363
+ ['schoolTransfer', 'studentRecords'].includes(this.values.requesterType)
364
+ },
365
+ recordsRequestTitle() {
366
+ return this.values.requesterType === 'schoolTransfer'
367
+ ? 'School Transfer Records Request'
368
+ : 'Student Records Request (FERPA Requests Only)'
369
+ },
370
+ requestTypeDescription() {
371
+ return this.serviceDefinition?.fields.find(field => field.name === 'requesterType')
372
+ ?.options.find(option => option.value === this.values.requesterType)?.description
373
+ },
289
374
  inlineIntroductionParts() {
290
375
  const introduction = this.formDefinition?.introduction || ''
291
376
  const link = this.formDefinition?.introductionLink
@@ -371,6 +456,7 @@ export default {
371
456
  this.validationErrors = {}
372
457
  this.validationAttempted = false
373
458
  this.honeypot = ''
459
+ this.showTranscriptReminder = false
374
460
 
375
461
  try {
376
462
  const expectedId = normalizeFormId(this.formId)
@@ -387,11 +473,13 @@ export default {
387
473
  )
388
474
  if (sequence !== this.loadSequence) return
389
475
 
390
- this.formDefinition = normalized
391
- this.values = createPublicFormValues(normalized)
476
+ this.serviceDefinition = normalized
477
+ this.formDefinition = studentRecordsPresentation(normalized)
478
+ this.values = createPublicFormValues(this.formDefinition)
392
479
  } catch (_error) {
393
480
  if (sequence !== this.loadSequence) return
394
481
  this.formDefinition = null
482
+ this.serviceDefinition = null
395
483
  this.definitionError =
396
484
  'This form could not be loaded. Please try again later or use another contact option on this page.'
397
485
  this.$nextTick(() => this.$refs.definitionError?.focus?.())
@@ -406,6 +494,15 @@ export default {
406
494
  return field.showCharacterCount === true
407
495
  },
408
496
  updateValue(name, value) {
497
+ if (this.isStudentRecordsForm && name === 'requesterType' && this.values[name] !== value) {
498
+ this.values.transferRecordTypes = []
499
+ this.values.otherRecordsDescription = ''
500
+ this.showTranscriptReminder = false
501
+ this.validationErrors = {}
502
+ this.validationAttempted = false
503
+ this.feedbackMessage = ''
504
+ this.submissionFailed = false
505
+ }
409
506
  this.values[name] = value
410
507
  for (const field of this.formDefinition.fields) {
411
508
  if (!isPublicFormFieldVisible(field, this.values)) {
@@ -416,7 +513,7 @@ export default {
416
513
  if (this.validationAttempted) this.validate(false)
417
514
  },
418
515
  validate(focusSummary = true) {
419
- if (!this.formDefinition) return false
516
+ if (!this.formDefinition || !this.showRequestFields) return false
420
517
  const result = validatePublicFormValues(this.formDefinition, this.values)
421
518
  this.values = result.values
422
519
  this.validationErrors = result.errors
@@ -428,12 +525,21 @@ export default {
428
525
  return result.valid
429
526
  },
430
527
  validateBeforeCaptcha(event) {
431
- if (this.sending || !this.validate()) {
528
+ if (this.sending || !this.validate() || this.checkTranscriptOnly()) {
432
529
  event?.preventDefault?.()
433
530
  return false
434
531
  }
435
532
  return true
436
533
  },
534
+ checkTranscriptOnly() {
535
+ if (this.isStudentRecordsForm &&
536
+ this.values.transferRecordTypes.length === 1 &&
537
+ this.values.transferRecordTypes[0] === 'academicRecords') {
538
+ this.showTranscriptReminder = true
539
+ return true
540
+ }
541
+ return false
542
+ },
437
543
  focusField(name) {
438
544
  document.getElementById(this.fieldId(name))?.focus?.()
439
545
  },
@@ -480,7 +586,7 @@ export default {
480
586
  this.$nextTick(() => this.$refs.feedback?.focus?.())
481
587
  },
482
588
  async submitForm(captchaToken = '') {
483
- if (this.sending || !this.validate()) return
589
+ if (this.sending || !this.validate() || this.checkTranscriptOnly()) return
484
590
  if (this.captchaEnabled && !captchaToken) {
485
591
  this.showFeedback(
486
592
  'Verification is required before submitting this form.'
@@ -494,8 +600,11 @@ export default {
494
600
  try {
495
601
  const payload = {
496
602
  ...(await serializePublicFormValues(
497
- this.formDefinition,
498
- this.values
603
+ this.serviceDefinition,
604
+ validatePublicFormValues(
605
+ this.serviceDefinition,
606
+ studentRecordsSubmissionValues(this.serviceDefinition, this.values)
607
+ ).values
499
608
  )),
500
609
  website: this.honeypot,
501
610
  captchaToken,
@@ -555,6 +664,12 @@ export default {
555
664
  align-items: flex-start;
556
665
  }
557
666
 
667
+ &__records-choice :deep(legend) {
668
+ font-size: 1.5rem;
669
+ font-weight: 700;
670
+ margin-bottom: 0.5rem;
671
+ }
672
+
558
673
  &__state,
559
674
  &__submit {
560
675
  align-items: center;
@@ -0,0 +1,72 @@
1
+ // Presentation for the unified form; the service still accepts its v8 wire fields.
2
+ export function usesStudentRecordsRouting(definition) {
3
+ return (
4
+ definition?.id === 'student-records-request' &&
5
+ definition.fields.some(field =>
6
+ field.name === 'requesterType' &&
7
+ field.options?.some(option => option.value === 'schoolTransfer')
8
+ ) &&
9
+ definition.fields.some(field => field.name === 'transferRecordTypes')
10
+ )
11
+ }
12
+
13
+ export function studentRecordsPresentation(definition) {
14
+ if (!usesStudentRecordsRouting(definition)) return definition
15
+
16
+ return {
17
+ ...definition,
18
+ fields: definition.fields
19
+ .filter(field => field.name !== 'recordsRequested')
20
+ .map(field => {
21
+ if (field.name === 'requesterType') {
22
+ return {
23
+ ...field,
24
+ label: 'What records do you need?',
25
+ options: [
26
+ {
27
+ label: 'Transcript or graduation verification only',
28
+ value: 'transcriptOnly',
29
+ },
30
+ { label: 'School transfer records', value: 'schoolTransfer' },
31
+ { label: 'Other student records', value: 'studentRecords' },
32
+ ],
33
+ }
34
+ }
35
+ if (field.name === 'transferRecordTypes') {
36
+ return {
37
+ ...field,
38
+ visibleWhen: {
39
+ fieldName: 'requesterType',
40
+ values: ['schoolTransfer', 'studentRecords'],
41
+ },
42
+ }
43
+ }
44
+ return field
45
+ }),
46
+ }
47
+ }
48
+
49
+ export function studentRecordsSubmissionValues(definition, values) {
50
+ if (
51
+ !usesStudentRecordsRouting(definition) ||
52
+ values.requesterType !== 'studentRecords'
53
+ ) {
54
+ return values
55
+ }
56
+
57
+ const options = definition.fields.find(
58
+ field => field.name === 'transferRecordTypes'
59
+ ).options
60
+ const labels = values.transferRecordTypes.map(value => {
61
+ const label = options.find(option => option.value === value).label
62
+ return value === 'otherRecords'
63
+ ? `${label}: ${values.otherRecordsDescription}`
64
+ : label
65
+ })
66
+ return {
67
+ ...values,
68
+ recordsRequested: labels.join('\n'),
69
+ transferRecordTypes: [],
70
+ otherRecordsDescription: '',
71
+ }
72
+ }