@asd20/ui-next 2.25.1 → 2.25.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 +14 -0
- package/package.json +1 -1
- package/src/components/molecules/Asd20PublicFormField/index.vue +70 -8
- package/src/components/organisms/Asd20PublicFormModal/index.vue +14 -6
- package/src/components/organisms/Asd20SiteSearch/index.vue +23 -6
- package/src/helpers/publicFormDefinition.js +22 -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.25.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.2...ui-next-v2.25.3) (2026-09-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add typo-tolerant serches and common aliases to search ([49f86a1](https://github.com/academydistrict20/asd20-ui-next/commit/49f86a104c8f33b3c2e53ebee696b95dbef43014))
|
|
9
|
+
|
|
10
|
+
## [2.25.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.1...ui-next-v2.25.2) (2026-09-23)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* refine public form presentation ([0e8d038](https://github.com/academydistrict20/asd20-ui-next/commit/0e8d0388783f522d65adeb6fb03f5c6a19c15456))
|
|
16
|
+
|
|
3
17
|
## [2.25.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.0...ui-next-v2.25.1) (2026-09-23)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
:required="field.required"
|
|
9
9
|
:minlength="field.minLength"
|
|
10
10
|
:maxlength="field.maxLength"
|
|
11
|
-
:show-character-count="
|
|
11
|
+
:show-character-count="false"
|
|
12
12
|
:show-pristine-validation-errors="showPristineValidationErrors"
|
|
13
13
|
:autocomplete="field.autocomplete || undefined"
|
|
14
14
|
:aria-describedby="describedBy"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
:minlength="field.minLength"
|
|
27
27
|
:maxlength="field.maxLength"
|
|
28
28
|
:max="dateMaximum"
|
|
29
|
-
:show-character-count="
|
|
29
|
+
:show-character-count="false"
|
|
30
30
|
:show-pristine-validation-errors="showPristineValidationErrors"
|
|
31
31
|
:autocomplete="field.autocomplete || undefined"
|
|
32
32
|
:aria-describedby="describedBy"
|
|
@@ -97,13 +97,22 @@
|
|
|
97
97
|
@update:modelValue="$emit('update:modelValue', $event)"
|
|
98
98
|
/>
|
|
99
99
|
|
|
100
|
-
<
|
|
101
|
-
v-if="field.helpText"
|
|
102
|
-
|
|
103
|
-
class="asd20-public-form-field__help"
|
|
100
|
+
<div
|
|
101
|
+
v-if="field.helpText || hasCharacterCount"
|
|
102
|
+
class="asd20-public-form-field__guidance"
|
|
104
103
|
>
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
<p
|
|
105
|
+
v-if="field.helpText"
|
|
106
|
+
:id="helpId"
|
|
107
|
+
class="asd20-public-form-field__help"
|
|
108
|
+
>
|
|
109
|
+
{{ field.helpText }}
|
|
110
|
+
</p>
|
|
111
|
+
<span
|
|
112
|
+
v-if="hasCharacterCount"
|
|
113
|
+
class="asd20-public-form-field__character-count"
|
|
114
|
+
>{{ characterCountText }}</span>
|
|
115
|
+
</div>
|
|
107
116
|
<ul
|
|
108
117
|
v-if="hasErrors"
|
|
109
118
|
:id="errorId"
|
|
@@ -174,6 +183,19 @@ export default {
|
|
|
174
183
|
hasErrors() {
|
|
175
184
|
return this.errors.length > 0
|
|
176
185
|
},
|
|
186
|
+
hasCharacterCount() {
|
|
187
|
+
return (
|
|
188
|
+
this.showCharacterCount &&
|
|
189
|
+
Number.isInteger(this.field.maxLength) &&
|
|
190
|
+
this.field.maxLength > 0 &&
|
|
191
|
+
typeof this.modelValue === 'string'
|
|
192
|
+
)
|
|
193
|
+
},
|
|
194
|
+
characterCountText() {
|
|
195
|
+
return this.hasCharacterCount
|
|
196
|
+
? `${this.modelValue.length}/${this.field.maxLength}`
|
|
197
|
+
: ''
|
|
198
|
+
},
|
|
177
199
|
describedBy() {
|
|
178
200
|
return (
|
|
179
201
|
[
|
|
@@ -226,15 +248,55 @@ export default {
|
|
|
226
248
|
}
|
|
227
249
|
|
|
228
250
|
&__help,
|
|
251
|
+
&__character-count,
|
|
229
252
|
&__errors {
|
|
230
253
|
color: var(--website-card__reverse-background-color);
|
|
231
254
|
font-size: 0.75rem;
|
|
232
255
|
margin: space(0.125) 0 0;
|
|
233
256
|
}
|
|
234
257
|
|
|
258
|
+
&__guidance {
|
|
259
|
+
align-items: flex-end;
|
|
260
|
+
display: flex;
|
|
261
|
+
gap: space(0.25);
|
|
262
|
+
justify-content: space-between;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
&__help {
|
|
266
|
+
flex: 1 1 auto;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
&__character-count {
|
|
270
|
+
flex: 0 0 auto;
|
|
271
|
+
margin-left: auto;
|
|
272
|
+
white-space: nowrap;
|
|
273
|
+
}
|
|
274
|
+
|
|
235
275
|
&__errors {
|
|
236
276
|
color: asd20-swatch('emergency', -1);
|
|
237
277
|
padding-left: space(1);
|
|
238
278
|
}
|
|
279
|
+
|
|
280
|
+
& :deep(.asd20-checkbox.asd20-checkbox__checkbox) {
|
|
281
|
+
align-items: flex-start;
|
|
282
|
+
gap: space(0.25);
|
|
283
|
+
|
|
284
|
+
input {
|
|
285
|
+
flex: 0 0 auto;
|
|
286
|
+
margin: 0.2rem 0 0;
|
|
287
|
+
opacity: 1;
|
|
288
|
+
position: static;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
label {
|
|
292
|
+
align-items: flex-start;
|
|
293
|
+
flex: 1 1 auto;
|
|
294
|
+
padding: 0;
|
|
295
|
+
|
|
296
|
+
span {
|
|
297
|
+
margin-right: 0;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
239
301
|
}
|
|
240
302
|
</style>
|
|
@@ -34,7 +34,11 @@
|
|
|
34
34
|
role="status"
|
|
35
35
|
aria-live="polite"
|
|
36
36
|
>
|
|
37
|
-
{{ formDefinition.successMessage }}
|
|
37
|
+
<p>{{ formDefinition.successMessage }}</p>
|
|
38
|
+
<template v-if="formDefinition.successDetails">
|
|
39
|
+
<hr />
|
|
40
|
+
<p>{{ formDefinition.successDetails }}</p>
|
|
41
|
+
</template>
|
|
38
42
|
</div>
|
|
39
43
|
|
|
40
44
|
<form
|
|
@@ -236,7 +240,7 @@ export default {
|
|
|
236
240
|
return this.formDefinition?.title || 'Public Form'
|
|
237
241
|
},
|
|
238
242
|
showPristineValidationErrors() {
|
|
239
|
-
return
|
|
243
|
+
return false
|
|
240
244
|
},
|
|
241
245
|
errorEntries() {
|
|
242
246
|
if (!this.formDefinition) return []
|
|
@@ -330,7 +334,7 @@ export default {
|
|
|
330
334
|
return `${this.instanceId}-${name}`
|
|
331
335
|
},
|
|
332
336
|
shouldShowCharacterCount(field) {
|
|
333
|
-
return
|
|
337
|
+
return field.showCharacterCount === true
|
|
334
338
|
},
|
|
335
339
|
updateValue(name, value) {
|
|
336
340
|
this.values[name] = value
|
|
@@ -459,8 +463,7 @@ export default {
|
|
|
459
463
|
}
|
|
460
464
|
|
|
461
465
|
&__error-summary,
|
|
462
|
-
&__feedback
|
|
463
|
-
&__success {
|
|
466
|
+
&__feedback {
|
|
464
467
|
font-weight: 600;
|
|
465
468
|
margin: space(0.5) 0;
|
|
466
469
|
}
|
|
@@ -476,7 +479,12 @@ export default {
|
|
|
476
479
|
|
|
477
480
|
&__success {
|
|
478
481
|
font-size: 1.1rem;
|
|
479
|
-
|
|
482
|
+
margin: space(0.5) 0;
|
|
483
|
+
text-align: left;
|
|
484
|
+
|
|
485
|
+
p {
|
|
486
|
+
margin: space(0.5) 0;
|
|
487
|
+
}
|
|
480
488
|
}
|
|
481
489
|
|
|
482
490
|
&__fallback {
|
|
@@ -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()
|
|
@@ -40,6 +40,12 @@ function optionalMaxDate(value, type) {
|
|
|
40
40
|
throw new Error('Maximum date is invalid.')
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function optionalBoolean(value, label) {
|
|
44
|
+
if (value == null) return false
|
|
45
|
+
if (typeof value !== 'boolean') throw new Error(`${label} is invalid.`)
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
function normalizeOptions(field) {
|
|
44
50
|
if (!['select', 'radio'].includes(field.type)) return undefined
|
|
45
51
|
if (
|
|
@@ -89,6 +95,10 @@ function normalizeField(field) {
|
|
|
89
95
|
maxLength: optionalLength(field.maxLength, 'Maximum length'),
|
|
90
96
|
maxDate: optionalMaxDate(field.maxDate, type),
|
|
91
97
|
autocomplete: boundedText(field.autocomplete, 'Autocomplete value', 100),
|
|
98
|
+
showCharacterCount: optionalBoolean(
|
|
99
|
+
field.showCharacterCount,
|
|
100
|
+
'Character-count visibility'
|
|
101
|
+
),
|
|
92
102
|
options: undefined,
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -99,6 +109,13 @@ function normalizeField(field) {
|
|
|
99
109
|
) {
|
|
100
110
|
throw new Error(`${normalized.label} has invalid length limits.`)
|
|
101
111
|
}
|
|
112
|
+
if (
|
|
113
|
+
normalized.showCharacterCount &&
|
|
114
|
+
(!['text', 'email', 'tel', 'textarea'].includes(type) ||
|
|
115
|
+
normalized.maxLength == null)
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(`${normalized.label} has an invalid character count.`)
|
|
118
|
+
}
|
|
102
119
|
|
|
103
120
|
normalized.options = normalizeOptions({
|
|
104
121
|
...field,
|
|
@@ -170,6 +187,11 @@ export function normalizePublicFormDefinition(definition) {
|
|
|
170
187
|
1000,
|
|
171
188
|
true
|
|
172
189
|
),
|
|
190
|
+
successDetails: boundedText(
|
|
191
|
+
definition.successDetails,
|
|
192
|
+
'Success details',
|
|
193
|
+
2000
|
|
194
|
+
),
|
|
173
195
|
fallbackContact: normalizedFallback,
|
|
174
196
|
fields,
|
|
175
197
|
}
|
|
@@ -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
|
+
}
|