@asd20/ui-next 2.35.2 → 2.35.4

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