@asd20/ui-next 2.25.2 → 2.26.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/Asd20PublicFormModal/index.vue +33 -0
- package/src/components/organisms/Asd20SiteSearch/index.vue +23 -6
- package/src/helpers/publicFormDefinition.js +20 -0
- package/src/helpers/queryFiles.js +2 -7
- package/src/helpers/queryMessages.js +2 -7
- package/src/helpers/queryPages.js +2 -7
- package/src/helpers/searchQuery.js +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.26.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.3...ui-next-v2.26.0) (2026-09-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add school transfer record request form with email workflow ([d92d458](https://github.com/academydistrict20/asd20-ui-next/commit/d92d458838303e7ae5ae600d26d6a6ce88fd748d))
|
|
9
|
+
|
|
10
|
+
## [2.25.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.2...ui-next-v2.25.3) (2026-09-25)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* add typo-tolerant serches and common aliases to search ([49f86a1](https://github.com/academydistrict20/asd20-ui-next/commit/49f86a104c8f33b3c2e53ebee696b95dbef43014))
|
|
16
|
+
|
|
3
17
|
## [2.25.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.1...ui-next-v2.25.2) (2026-09-23)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
<form
|
|
45
45
|
v-else-if="formDefinition"
|
|
46
|
+
:class="{
|
|
47
|
+
'asd20-public-form-modal__records-form': usesRecordsFormSpacing,
|
|
48
|
+
}"
|
|
46
49
|
novalidate
|
|
47
50
|
@submit.prevent="validateBeforeCaptcha"
|
|
48
51
|
>
|
|
@@ -52,6 +55,11 @@
|
|
|
52
55
|
>
|
|
53
56
|
{{ formDefinition.introduction }}
|
|
54
57
|
</p>
|
|
58
|
+
<p v-if="formDefinition.introductionLink">
|
|
59
|
+
<a :href="formDefinition.introductionLink.href">
|
|
60
|
+
{{ formDefinition.introductionLink.label }}
|
|
61
|
+
</a>
|
|
62
|
+
</p>
|
|
55
63
|
|
|
56
64
|
<div
|
|
57
65
|
v-if="errorEntries.length"
|
|
@@ -77,6 +85,11 @@
|
|
|
77
85
|
<asd20-public-form-field
|
|
78
86
|
v-for="field in formDefinition.fields"
|
|
79
87
|
:key="field.name"
|
|
88
|
+
:class="{
|
|
89
|
+
'asd20-public-form-modal__requester-choice':
|
|
90
|
+
formDefinition.id === 'student-records-request' &&
|
|
91
|
+
field.name === 'requesterType',
|
|
92
|
+
}"
|
|
80
93
|
:field="field"
|
|
81
94
|
:input-id="fieldId(field.name)"
|
|
82
95
|
:model-value="values[field.name]"
|
|
@@ -239,6 +252,12 @@ export default {
|
|
|
239
252
|
modalTitle() {
|
|
240
253
|
return this.formDefinition?.title || 'Public Form'
|
|
241
254
|
},
|
|
255
|
+
usesRecordsFormSpacing() {
|
|
256
|
+
return [
|
|
257
|
+
'student-records-request',
|
|
258
|
+
'school-transfer-transcript-request',
|
|
259
|
+
].includes(this.formDefinition?.id)
|
|
260
|
+
},
|
|
242
261
|
showPristineValidationErrors() {
|
|
243
262
|
return false
|
|
244
263
|
},
|
|
@@ -453,6 +472,20 @@ export default {
|
|
|
453
472
|
font-size: 1rem;
|
|
454
473
|
}
|
|
455
474
|
|
|
475
|
+
&__records-form :deep(.asd20-text-input),
|
|
476
|
+
&__records-form :deep(.asd20-text-area-input) {
|
|
477
|
+
margin-bottom: 0.25rem;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
&__records-form :deep(.asd20-public-form-field) {
|
|
481
|
+
margin-bottom: 1rem;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
&__requester-choice.asd20-public-form-field
|
|
485
|
+
:deep(fieldset.asd20-public-form-field__choices label) {
|
|
486
|
+
align-items: flex-start;
|
|
487
|
+
}
|
|
488
|
+
|
|
456
489
|
&__state,
|
|
457
490
|
&__submit {
|
|
458
491
|
align-items: center;
|
|
@@ -2917,11 +2917,9 @@ export default {
|
|
|
2917
2917
|
this.skipKeywordWatcher = true
|
|
2918
2918
|
this.keywords = this.limitWords(text, 4)
|
|
2919
2919
|
await this.searchPages({ keepAnswersTab: false, runId })
|
|
2920
|
-
this.searchFilesWithGroupOwners({
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
)
|
|
2924
|
-
.catch(() => {})
|
|
2920
|
+
const supportingSearchPromise = this.searchFilesWithGroupOwners({
|
|
2921
|
+
runId,
|
|
2922
|
+
}).catch(() => [])
|
|
2925
2923
|
if (!this.isCurrentSearchRun(runId)) return
|
|
2926
2924
|
this.currentTab = 'Pages'
|
|
2927
2925
|
this.syncSearchState({ text, mode, tab: 'Pages' })
|
|
@@ -2929,6 +2927,10 @@ export default {
|
|
|
2929
2927
|
|
|
2930
2928
|
// Log keyword-only search analytics
|
|
2931
2929
|
const keywordAnalyticsPages = this.getAnalyticsPages()
|
|
2930
|
+
await supportingSearchPromise
|
|
2931
|
+
if (!this.isCurrentSearchRun(runId)) return
|
|
2932
|
+
this.selectBestKeywordTabAfterContactResults({ runId, text })
|
|
2933
|
+
const resultCounts = this.getKeywordAnalyticsResultCounts()
|
|
2932
2934
|
const keywordLogPages = keywordAnalyticsPages
|
|
2933
2935
|
.slice(0, 10)
|
|
2934
2936
|
.map(p => ({ title: p.title || '', url: p.url || '' }))
|
|
@@ -2936,7 +2938,7 @@ export default {
|
|
|
2936
2938
|
question: text,
|
|
2937
2939
|
searchType: 'keyword_only',
|
|
2938
2940
|
keywords: this.keywords || text,
|
|
2939
|
-
hadResults:
|
|
2941
|
+
hadResults: resultCounts.total > 0,
|
|
2940
2942
|
relatedPages: keywordLogPages,
|
|
2941
2943
|
organizationId:
|
|
2942
2944
|
this.organization && this.organization.id
|
|
@@ -3572,6 +3574,21 @@ export default {
|
|
|
3572
3574
|
)
|
|
3573
3575
|
},
|
|
3574
3576
|
|
|
3577
|
+
getKeywordAnalyticsResultCounts() {
|
|
3578
|
+
const pages = this.getAnalyticsPages().length
|
|
3579
|
+
const files = Array.isArray(this.currentFiles)
|
|
3580
|
+
? this.currentFiles.length
|
|
3581
|
+
: 0
|
|
3582
|
+
const contacts = Number(this.contactResultCount) || 0
|
|
3583
|
+
|
|
3584
|
+
return {
|
|
3585
|
+
pages,
|
|
3586
|
+
files,
|
|
3587
|
+
contacts,
|
|
3588
|
+
total: pages + files + contacts,
|
|
3589
|
+
}
|
|
3590
|
+
},
|
|
3591
|
+
|
|
3575
3592
|
limitWords(text, maxWords = 4) {
|
|
3576
3593
|
return (text || '')
|
|
3577
3594
|
.trim()
|
|
@@ -171,6 +171,25 @@ export function normalizePublicFormDefinition(definition) {
|
|
|
171
171
|
const id = boundedText(definition.id, 'Form ID', 100, true)
|
|
172
172
|
if (!FORM_ID_PATTERN.test(id)) throw new Error('The public form ID is invalid.')
|
|
173
173
|
|
|
174
|
+
let introductionLink
|
|
175
|
+
if (definition.introductionLink != null) {
|
|
176
|
+
const label = boundedText(
|
|
177
|
+
definition.introductionLink.label,
|
|
178
|
+
'Introduction link label',
|
|
179
|
+
200,
|
|
180
|
+
true
|
|
181
|
+
)
|
|
182
|
+
const href = boundedText(
|
|
183
|
+
definition.introductionLink.href,
|
|
184
|
+
'Introduction link URL',
|
|
185
|
+
500,
|
|
186
|
+
true
|
|
187
|
+
)
|
|
188
|
+
if (!/^\/[a-z0-9/-]+\/?$/.test(href) || href.startsWith('//'))
|
|
189
|
+
throw new Error('The introduction link must be a local page path.')
|
|
190
|
+
introductionLink = { label, href }
|
|
191
|
+
}
|
|
192
|
+
|
|
174
193
|
return {
|
|
175
194
|
id,
|
|
176
195
|
version,
|
|
@@ -180,6 +199,7 @@ export function normalizePublicFormDefinition(definition) {
|
|
|
180
199
|
'Form introduction',
|
|
181
200
|
2000
|
|
182
201
|
),
|
|
202
|
+
introductionLink,
|
|
183
203
|
submitLabel: boundedText(definition.submitLabel, 'Submit label', 100, true),
|
|
184
204
|
successMessage: boundedText(
|
|
185
205
|
definition.successMessage,
|
|
@@ -2,6 +2,7 @@ import axios from 'axios'
|
|
|
2
2
|
|
|
3
3
|
import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
|
|
4
4
|
import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
|
|
5
|
+
import { buildTypoTolerantSearchQuery } from './searchQuery'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Creates a Azure Search payload
|
|
@@ -13,13 +14,7 @@ function searchPayload(state) {
|
|
|
13
14
|
let query = '*'
|
|
14
15
|
|
|
15
16
|
if (state.enableFuzzySearch) {
|
|
16
|
-
|
|
17
|
-
query = state.keywords.trim()
|
|
18
|
-
? `"${state.keywords.trim()}" OR name:${state.keywords
|
|
19
|
-
.trim()
|
|
20
|
-
.split(' ')
|
|
21
|
-
.join('* OR name:')}*`
|
|
22
|
-
: '*'
|
|
17
|
+
query = buildTypoTolerantSearchQuery(state.keywords, 'name')
|
|
23
18
|
} else {
|
|
24
19
|
query = state.keywords.trim()
|
|
25
20
|
}
|
|
@@ -2,6 +2,7 @@ import axios from 'axios'
|
|
|
2
2
|
|
|
3
3
|
import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
|
|
4
4
|
import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
|
|
5
|
+
import { buildTypoTolerantSearchQuery } from './searchQuery'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Creates a Azure Search payload
|
|
@@ -43,13 +44,7 @@ function searchPayload(state) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (state.enableFuzzySearch) {
|
|
46
|
-
|
|
47
|
-
query = state.keywords.trim()
|
|
48
|
-
? `"${state.keywords.trim()}" OR title:${state.keywords
|
|
49
|
-
.trim()
|
|
50
|
-
.split(' ')
|
|
51
|
-
.join('* OR title:')}*`
|
|
52
|
-
: '*'
|
|
47
|
+
query = buildTypoTolerantSearchQuery(state.keywords, 'title')
|
|
53
48
|
} else {
|
|
54
49
|
query = state.keywords.trim()
|
|
55
50
|
}
|
|
@@ -2,6 +2,7 @@ import axios from 'axios'
|
|
|
2
2
|
|
|
3
3
|
import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
|
|
4
4
|
import { resolveRuntimeConfigValue } from './runtimeConfig'
|
|
5
|
+
import { buildTypoTolerantSearchQuery } from './searchQuery'
|
|
5
6
|
|
|
6
7
|
export const NO_PAGE_RESULTS_FALLBACKS = [
|
|
7
8
|
{
|
|
@@ -56,13 +57,7 @@ function searchPayload(state) {
|
|
|
56
57
|
let query = '*'
|
|
57
58
|
|
|
58
59
|
if (state.enableFuzzySearch) {
|
|
59
|
-
|
|
60
|
-
query = state.keywords.trim()
|
|
61
|
-
? `"${state.keywords.trim()}" OR title:${state.keywords
|
|
62
|
-
.trim()
|
|
63
|
-
.split(' ')
|
|
64
|
-
.join('* OR title:')}*`
|
|
65
|
-
: '*'
|
|
60
|
+
query = buildTypoTolerantSearchQuery(state.keywords, 'title')
|
|
66
61
|
} else {
|
|
67
62
|
query = state.keywords.trim()
|
|
68
63
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const MIN_FUZZY_TOKEN_LENGTH = 5
|
|
2
|
+
|
|
3
|
+
const SEARCH_ALIASES = Object.freeze({
|
|
4
|
+
childfind: ['child find'],
|
|
5
|
+
dresscode: ['dress code'],
|
|
6
|
+
foodmenu: ['food menu', 'lunch menu'],
|
|
7
|
+
hoco: ['homecoming'],
|
|
8
|
+
landsharks: ['land sharks'],
|
|
9
|
+
merch: ['merchandise', 'spirit wear'],
|
|
10
|
+
powderpuff: ['powder puff'],
|
|
11
|
+
printshop: ['print shop'],
|
|
12
|
+
studenthub: ['student hub'],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function escapePhrase(value) {
|
|
16
|
+
return value.replace(/"/g, '\\"')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function searchTokens(value) {
|
|
20
|
+
return Array.from(
|
|
21
|
+
new Set(
|
|
22
|
+
String(value || '')
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.map(token => token.replace(/[^a-z0-9-]/gi, '').trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function prefixClause(tokens, field = '') {
|
|
31
|
+
if (!tokens.length) return ''
|
|
32
|
+
const prefix = field ? `${field}:` : ''
|
|
33
|
+
return tokens.map(token => `${prefix}${token}*`).join(' AND ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fuzzyClause(tokens) {
|
|
37
|
+
if (!tokens.some(token => token.length >= MIN_FUZZY_TOKEN_LENGTH)) return ''
|
|
38
|
+
return tokens
|
|
39
|
+
.map(token =>
|
|
40
|
+
token.length >= MIN_FUZZY_TOKEN_LENGTH ? `${token}~1` : token
|
|
41
|
+
)
|
|
42
|
+
.join(' AND ')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildTypoTolerantSearchQuery(keywords, field = '') {
|
|
46
|
+
const trimmed = String(keywords || '').trim()
|
|
47
|
+
if (!trimmed) return '*'
|
|
48
|
+
|
|
49
|
+
const tokens = searchTokens(trimmed)
|
|
50
|
+
if (!tokens.length) return `"${escapePhrase(trimmed)}"`
|
|
51
|
+
|
|
52
|
+
const clauses = [
|
|
53
|
+
`"${escapePhrase(trimmed)}"`,
|
|
54
|
+
prefixClause(tokens, field),
|
|
55
|
+
prefixClause(tokens),
|
|
56
|
+
fuzzyClause(tokens),
|
|
57
|
+
]
|
|
58
|
+
const aliases = SEARCH_ALIASES[trimmed.toLowerCase()] || []
|
|
59
|
+
|
|
60
|
+
aliases.forEach(alias => {
|
|
61
|
+
const aliasTokens = searchTokens(alias)
|
|
62
|
+
clauses.push(`"${escapePhrase(alias)}"`)
|
|
63
|
+
clauses.push(prefixClause(aliasTokens, field))
|
|
64
|
+
clauses.push(prefixClause(aliasTokens))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return Array.from(new Set(clauses.filter(Boolean)))
|
|
68
|
+
.map(clause => (clause.includes(' AND ') ? `(${clause})` : clause))
|
|
69
|
+
.join(' OR ')
|
|
70
|
+
}
|