@asd20/ui-next 2.2.0 → 2.2.2

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.2.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.2.1...ui-next-v2.2.2) (2026-04-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fix log search analytics not executing ([5c94742](https://github.com/academydistrict20/asd20-ui-next/commit/5c94742ed63517c5f5ba918583b052606951a842))
9
+
10
+ ## [2.2.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.2.0...ui-next-v2.2.1) (2026-04-07)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * fix file header text being dropped ([cb07594](https://github.com/academydistrict20/asd20-ui-next/commit/cb075946768dd58fb39ceed08ae406a1c36a532f))
16
+
3
17
  # [2.2.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.1.0...ui-next-v2.2.0) (2026-04-07)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -3,10 +3,10 @@
3
3
  v-bind="$attrs"
4
4
  ref="listComponent"
5
5
  role="region"
6
- :aria-label="title"
6
+ :aria-label="resolvedTitle"
7
7
  class="asd20-file-list"
8
8
  :max-height="maxHeight"
9
- :headline="title"
9
+ :headline="resolvedTitle"
10
10
  :icon="icon"
11
11
  :column-width="640"
12
12
  >
@@ -19,7 +19,6 @@
19
19
  v-model="searchQuery"
20
20
  class="asd20-file-list__search"
21
21
  :placeholder="searchPlaceholder"
22
- :extra="resultSummary"
23
22
  :id-tag="searchIdTag"
24
23
  />
25
24
  </div>
@@ -142,6 +141,7 @@ export default {
142
141
  props: {
143
142
  files: { type: Array, default: () => [] },
144
143
  title: { type: String, default: '' },
144
+ headline: { type: String, default: '' },
145
145
  icon: { type: String, default: '' },
146
146
  url: { type: String, default: '' },
147
147
  groupByOwner: { type: Boolean, default: false },
@@ -166,6 +166,10 @@ export default {
166
166
  }),
167
167
 
168
168
  computed: {
169
+ resolvedTitle() {
170
+ return this.title || this.headline
171
+ },
172
+
169
173
  computedFiles() {
170
174
  return (this.files || []).concat(this.loadedFiles)
171
175
  },
@@ -230,13 +234,6 @@ export default {
230
234
  return this.filteredFiles.slice(0, this.effectiveVisibleCount)
231
235
  },
232
236
 
233
- paginatedFiles() {
234
- if (!this.paginate) return this.visibleFiles
235
-
236
- const startIndex = (this.currentPage - 1) * this.resolvedPageSize
237
- return this.filteredFiles.slice(startIndex, startIndex + this.resolvedPageSize)
238
- },
239
-
240
237
  canShowMore() {
241
238
  return (
242
239
  !!this.initialVisibleCount &&
@@ -261,16 +258,6 @@ export default {
261
258
  : this.initialVisibleCount || 50
262
259
  },
263
260
 
264
- resultSummary() {
265
- if (!this.searchable) return ''
266
- if (!this.totalFilesCount) return '0 files'
267
- if (this.hasActiveSearch) {
268
- return `${this.filteredFilesCount} of ${this.totalFilesCount}`
269
- }
270
-
271
- return `${this.totalFilesCount} files`
272
- },
273
-
274
261
  footerSummary() {
275
262
  if (!this.totalFilesCount) return ''
276
263
 
@@ -306,7 +293,7 @@ export default {
306
293
  },
307
294
 
308
295
  searchIdTag() {
309
- return normalizeSearchText(this.title).replace(/\s+/g, '-')
296
+ return normalizeSearchText(this.resolvedTitle).replace(/\s+/g, '-')
310
297
  },
311
298
 
312
299
  emptyStateMessage() {
@@ -344,7 +331,7 @@ export default {
344
331
  return `Showing ${this.filteredFilesCount} files.`
345
332
  },
346
333
 
347
- categorizedFileItems() {
334
+ groupedFileItems() {
348
335
  if (this.groupByDate) {
349
336
  return this.fileItemsGroupedByDate
350
337
  } else if (this.groupByOwner) {
@@ -357,8 +344,41 @@ export default {
357
344
  return this.fileItemsGroupedByCategory
358
345
  },
359
346
 
347
+ paginatedGroupEntries() {
348
+ const entries = this.groupedFileItems.flatMap(group =>
349
+ group.items.map(item => ({
350
+ name: group.name,
351
+ item,
352
+ }))
353
+ )
354
+
355
+ if (!this.paginate) return entries
356
+
357
+ const startIndex = (this.currentPage - 1) * this.resolvedPageSize
358
+ return entries.slice(startIndex, startIndex + this.resolvedPageSize)
359
+ },
360
+
361
+ categorizedFileItems() {
362
+ if (!this.paginate) return this.groupedFileItems
363
+
364
+ return this.paginatedGroupEntries.reduce((groups, entry) => {
365
+ const existingGroup = groups.find(group => group.name === entry.name)
366
+
367
+ if (existingGroup) {
368
+ existingGroup.items.push(entry.item)
369
+ return groups
370
+ }
371
+
372
+ groups.push({
373
+ name: entry.name,
374
+ items: [entry.item],
375
+ })
376
+ return groups
377
+ }, [])
378
+ },
379
+
360
380
  fileItemsGroupedByCategory() {
361
- return this.paginatedFiles
381
+ return this.visibleFiles
362
382
  .reduce((a, c) => {
363
383
  let categories =
364
384
  c.categories && c.categories.length > 0
@@ -373,21 +393,19 @@ export default {
373
393
  .map(c => {
374
394
  return {
375
395
  name: c,
376
- items: mapFilesToListItems(
377
- this.paginatedFiles
378
- .map(f => ({
379
- ...f,
380
- categories:
381
- f.categories && f.categories.length > 0
382
- ? f.categories
383
- : ['Uncategorized'],
384
- }))
385
- .filter(f => f.categories.indexOf(c) > -1)
386
- )
387
- .sort((a, b) =>
388
- a.label > b.label ? 1 : b.label > a.label ? -1 : 0
396
+ items: this.sortMappedFileItems(
397
+ mapFilesToListItems(
398
+ this.visibleFiles
399
+ .map(f => ({
400
+ ...f,
401
+ categories:
402
+ f.categories && f.categories.length > 0
403
+ ? f.categories
404
+ : ['Uncategorized'],
405
+ }))
406
+ .filter(f => f.categories.indexOf(c) > -1)
389
407
  )
390
- .map(t => ({
408
+ ).map(t => ({
391
409
  ...t,
392
410
  bordered: false,
393
411
  alignTop: false,
@@ -399,7 +417,7 @@ export default {
399
417
  },
400
418
 
401
419
  fileItemsGroupedByCategoryDescending() {
402
- return this.paginatedFiles
420
+ return this.visibleFiles
403
421
  .reduce((a, c) => {
404
422
  let categories =
405
423
  c.categories && c.categories.length > 0
@@ -414,21 +432,19 @@ export default {
414
432
  .map(c => {
415
433
  return {
416
434
  name: c,
417
- items: mapFilesToListItems(
418
- this.paginatedFiles
419
- .map(f => ({
420
- ...f,
421
- categories:
422
- f.categories && f.categories.length > 0
423
- ? f.categories
424
- : ['Uncategorized'],
425
- }))
426
- .filter(f => f.categories.indexOf(c) > -1)
427
- )
428
- .sort((a, b) =>
429
- a.label > b.label ? 1 : b.label > a.label ? -1 : 0
435
+ items: this.sortMappedFileItems(
436
+ mapFilesToListItems(
437
+ this.visibleFiles
438
+ .map(f => ({
439
+ ...f,
440
+ categories:
441
+ f.categories && f.categories.length > 0
442
+ ? f.categories
443
+ : ['Uncategorized'],
444
+ }))
445
+ .filter(f => f.categories.indexOf(c) > -1)
430
446
  )
431
- .map(t => ({
447
+ ).map(t => ({
432
448
  ...t,
433
449
  bordered: false,
434
450
  alignTop: false,
@@ -440,7 +456,7 @@ export default {
440
456
  },
441
457
 
442
458
  fileItemsGroupedByTag() {
443
- return this.paginatedFiles
459
+ return this.visibleFiles
444
460
  .reduce((a, t) => {
445
461
  let tags = t.tags && t.tags.length > 0 ? t.tags : ['Untagged']
446
462
  for (const tag of tags) {
@@ -452,18 +468,16 @@ export default {
452
468
  .map(t => {
453
469
  return {
454
470
  name: t,
455
- items: mapFilesToListItems(
456
- this.paginatedFiles
457
- .map(f => ({
458
- ...f,
459
- tags: f.tags && f.tags.length > 0 ? f.tags : ['Untagged'],
460
- }))
461
- .filter(f => f.tags.indexOf(t) > -1)
462
- )
463
- .sort((a, b) =>
464
- a.label > b.label ? 1 : b.label > a.label ? -1 : 0
471
+ items: this.sortMappedFileItems(
472
+ mapFilesToListItems(
473
+ this.visibleFiles
474
+ .map(f => ({
475
+ ...f,
476
+ tags: f.tags && f.tags.length > 0 ? f.tags : ['Untagged'],
477
+ }))
478
+ .filter(f => f.tags.indexOf(t) > -1)
465
479
  )
466
- .map(t => ({
480
+ ).map(t => ({
467
481
  ...t,
468
482
  bordered: false,
469
483
  alignTop: false,
@@ -475,7 +489,7 @@ export default {
475
489
  },
476
490
 
477
491
  fileItemsGroupedByDate() {
478
- return this.paginatedFiles
492
+ return this.visibleFiles
479
493
  .reduce((a, c) => {
480
494
  let date = new Date(c.lastModifiedDateTime).toLocaleDateString()
481
495
  if (a.indexOf(date) === -1) a.push(date)
@@ -485,15 +499,13 @@ export default {
485
499
  return {
486
500
  name: d,
487
501
  unix: new Date(d).valueOf(),
488
- items: mapFilesToListItems(
489
- this.paginatedFiles.filter(
490
- f => new Date(f.lastModifiedDateTime).toLocaleDateString() === d
491
- )
492
- )
493
- .sort((a, b) =>
494
- a.label > b.label ? 1 : b.label > a.label ? -1 : 0
502
+ items: this.sortMappedFileItems(
503
+ mapFilesToListItems(
504
+ this.visibleFiles.filter(
505
+ f => new Date(f.lastModifiedDateTime).toLocaleDateString() === d
506
+ )
495
507
  )
496
- .map(t => ({
508
+ ).map(t => ({
497
509
  ...t,
498
510
  bordered: false,
499
511
  alignTop: false,
@@ -508,7 +520,9 @@ export default {
508
520
  fileItemsGroupedByOwner() {
509
521
  return [
510
522
  {
511
- items: mapFilesToListItems(this.paginatedFiles).map(t => ({
523
+ items: this.sortMappedFileItems(
524
+ mapFilesToListItems(this.visibleFiles)
525
+ ).map(t => ({
512
526
  ...t,
513
527
  bordered: false,
514
528
  alignTop: false,
@@ -584,6 +598,13 @@ export default {
584
598
  const haystack = this.getFileSearchTokens(file)
585
599
  return this.searchTerms.every(term => haystack.includes(term))
586
600
  },
601
+ sortMappedFileItems(items = []) {
602
+ return [...items].sort((a, b) => {
603
+ const left = String(a.label || '').toLowerCase()
604
+ const right = String(b.label || '').toLowerCase()
605
+ return left.localeCompare(right)
606
+ })
607
+ },
587
608
  // Expose the checkForOverflow method from the asd20viewport component
588
609
  // Expose the handleResize method from the asd20list component
589
610
  checkForOverflow() {
@@ -17,11 +17,51 @@
17
17
  :name="icon"
18
18
  :size="iconSize"
19
19
  />
20
- <component
21
- :is="headlineTag"
20
+ <h1
21
+ v-if="headlineTag === 'h1'"
22
22
  class="asd20-list__headline"
23
23
  v-html="headline"
24
- ></component>
24
+ ></h1>
25
+ <h2
26
+ v-else-if="headlineTag === 'h2'"
27
+ class="asd20-list__headline"
28
+ v-html="headline"
29
+ ></h2>
30
+ <h3
31
+ v-else-if="headlineTag === 'h3'"
32
+ class="asd20-list__headline"
33
+ v-html="headline"
34
+ ></h3>
35
+ <h4
36
+ v-else-if="headlineTag === 'h4'"
37
+ class="asd20-list__headline"
38
+ v-html="headline"
39
+ ></h4>
40
+ <h5
41
+ v-else-if="headlineTag === 'h5'"
42
+ class="asd20-list__headline"
43
+ v-html="headline"
44
+ ></h5>
45
+ <h6
46
+ v-else-if="headlineTag === 'h6'"
47
+ class="asd20-list__headline"
48
+ v-html="headline"
49
+ ></h6>
50
+ <p
51
+ v-else-if="headlineTag === 'p'"
52
+ class="asd20-list__headline"
53
+ v-html="headline"
54
+ ></p>
55
+ <span
56
+ v-else-if="headlineTag === 'span'"
57
+ class="asd20-list__headline"
58
+ v-html="headline"
59
+ ></span>
60
+ <div
61
+ v-else
62
+ class="asd20-list__headline"
63
+ v-html="headline"
64
+ ></div>
25
65
  </div>
26
66
  <slot name="header" />
27
67
  </div>
@@ -979,8 +979,9 @@ export default {
979
979
  },
980
980
  feedbackFormUrlRaw() {
981
981
  if (this.aiSearchFeedbackFormUrl) return this.aiSearchFeedbackFormUrl
982
- if (this.$config && this.$config.aiSearchFeedbackFormUrl)
983
- return this.$config.aiSearchFeedbackFormUrl
982
+ const runtimeConfig = this.resolveRuntimeConfig()
983
+ if (runtimeConfig.aiSearchFeedbackFormUrl)
984
+ return runtimeConfig.aiSearchFeedbackFormUrl
984
985
  return ''
985
986
  },
986
987
 
@@ -2213,6 +2214,28 @@ export default {
2213
2214
  this.$emit('update:active', false)
2214
2215
  },
2215
2216
 
2217
+ resolveRuntimeConfig() {
2218
+ const runtimeConfig =
2219
+ this.$config ||
2220
+ this.$store?.$config ||
2221
+ this.$?.appContext?.config?.globalProperties?.$config ||
2222
+ {}
2223
+
2224
+ if (
2225
+ runtimeConfig.public &&
2226
+ typeof runtimeConfig.public === 'object' &&
2227
+ !Array.isArray(runtimeConfig.public)
2228
+ ) {
2229
+ return runtimeConfig.public
2230
+ }
2231
+
2232
+ return runtimeConfig
2233
+ },
2234
+
2235
+ resolveFunctionsEndpoint() {
2236
+ return this.resolveRuntimeConfig().functionsEndpoint || ''
2237
+ },
2238
+
2216
2239
  toggleKeywords(turnId) {
2217
2240
  this.expandedKeywordsTurnId =
2218
2241
  this.expandedKeywordsTurnId === turnId ? null : turnId
@@ -2253,10 +2276,7 @@ export default {
2253
2276
  const response = await logSearchFeedback({
2254
2277
  searchLogId: turn.searchLogId,
2255
2278
  feedbackValue,
2256
- functionsEndpoint:
2257
- this.$config && this.$config.functionsEndpoint
2258
- ? this.$config.functionsEndpoint
2259
- : null,
2279
+ functionsEndpoint: this.resolveFunctionsEndpoint() || null,
2260
2280
  })
2261
2281
 
2262
2282
  if (response && response.ok) {
@@ -2330,10 +2350,7 @@ export default {
2330
2350
  const response = await logSearchFeedback({
2331
2351
  searchLogId: targetTurn.searchLogId,
2332
2352
  improvementFeedback,
2333
- functionsEndpoint:
2334
- this.$config && this.$config.functionsEndpoint
2335
- ? this.$config.functionsEndpoint
2336
- : null,
2353
+ functionsEndpoint: this.resolveFunctionsEndpoint() || null,
2337
2354
  })
2338
2355
 
2339
2356
  if (response && response.ok) {
@@ -2543,10 +2560,7 @@ export default {
2543
2560
  : null,
2544
2561
  languageCode: this.getSearchLanguageCode(),
2545
2562
  isFollowup: false,
2546
- functionsEndpoint:
2547
- this.$config && this.$config.functionsEndpoint
2548
- ? this.$config.functionsEndpoint
2549
- : null,
2563
+ functionsEndpoint: this.resolveFunctionsEndpoint() || null,
2550
2564
  })
2551
2565
  }
2552
2566
  },
@@ -2797,10 +2811,7 @@ export default {
2797
2811
  : null,
2798
2812
  languageCode: this.getSearchLanguageCode(),
2799
2813
  isFollowup: isFollowUpQuestion,
2800
- functionsEndpoint:
2801
- this.$config && this.$config.functionsEndpoint
2802
- ? this.$config.functionsEndpoint
2803
- : null,
2814
+ functionsEndpoint: this.resolveFunctionsEndpoint() || null,
2804
2815
  aiResponse: assistantResponseForAnalytics || null,
2805
2816
  })
2806
2817
  if (assistantTurnId) {
@@ -17,11 +17,51 @@
17
17
  :name="icon"
18
18
  :size="iconSize"
19
19
  />
20
- <component
21
- :is="headlineTag"
20
+ <h1
21
+ v-if="headlineTag === 'h1'"
22
22
  class="asd20-list__headline"
23
23
  v-html="headline"
24
- ></component>
24
+ ></h1>
25
+ <h2
26
+ v-else-if="headlineTag === 'h2'"
27
+ class="asd20-list__headline"
28
+ v-html="headline"
29
+ ></h2>
30
+ <h3
31
+ v-else-if="headlineTag === 'h3'"
32
+ class="asd20-list__headline"
33
+ v-html="headline"
34
+ ></h3>
35
+ <h4
36
+ v-else-if="headlineTag === 'h4'"
37
+ class="asd20-list__headline"
38
+ v-html="headline"
39
+ ></h4>
40
+ <h5
41
+ v-else-if="headlineTag === 'h5'"
42
+ class="asd20-list__headline"
43
+ v-html="headline"
44
+ ></h5>
45
+ <h6
46
+ v-else-if="headlineTag === 'h6'"
47
+ class="asd20-list__headline"
48
+ v-html="headline"
49
+ ></h6>
50
+ <p
51
+ v-else-if="headlineTag === 'p'"
52
+ class="asd20-list__headline"
53
+ v-html="headline"
54
+ ></p>
55
+ <span
56
+ v-else-if="headlineTag === 'span'"
57
+ class="asd20-list__headline"
58
+ v-html="headline"
59
+ ></span>
60
+ <div
61
+ v-else
62
+ class="asd20-list__headline"
63
+ v-html="headline"
64
+ ></div>
25
65
  </div>
26
66
  <slot name="header" />
27
67
  </div>
@@ -1,5 +1,12 @@
1
1
  import axios from 'axios'
2
2
 
3
+ function resolveNuxtWindowConfig() {
4
+ if (typeof window === 'undefined' || !window.__NUXT__) return {}
5
+
6
+ const runtimeConfig = window.__NUXT__.config
7
+ return runtimeConfig && typeof runtimeConfig === 'object' ? runtimeConfig : {}
8
+ }
9
+
3
10
  export function resolveFunctionsEndpoint(payload) {
4
11
  const explicitEndpoint =
5
12
  payload && typeof payload.functionsEndpoint === 'string'
@@ -13,10 +20,15 @@ export function resolveFunctionsEndpoint(payload) {
13
20
  if (processEndpoint) return processEndpoint
14
21
  }
15
22
 
16
- if (typeof window !== 'undefined' && window.__NUXT__ && window.__NUXT__.config) {
17
- const nuxtEndpoint = window.__NUXT__.config.functionsEndpoint
18
- if (typeof nuxtEndpoint === 'string' && nuxtEndpoint.trim()) {
19
- return nuxtEndpoint.trim()
23
+ const nuxtConfig = resolveNuxtWindowConfig()
24
+ const nuxtEndpoints = [
25
+ nuxtConfig.functionsEndpoint,
26
+ nuxtConfig.public && nuxtConfig.public.functionsEndpoint,
27
+ ]
28
+
29
+ for (const candidate of nuxtEndpoints) {
30
+ if (typeof candidate === 'string' && candidate.trim()) {
31
+ return candidate.trim()
20
32
  }
21
33
  }
22
34
 
@@ -1,6 +1,8 @@
1
1
  // helpers/search/queryAiSite.js
2
2
  import axios from 'axios'
3
3
 
4
+ import { resolveFunctionsEndpoint } from './logSearchAnalytics'
5
+
4
6
  /**
5
7
  * Calls Azure Function / API for AI site search.
6
8
  * Expected response shape:
@@ -14,14 +16,20 @@ export default async function queryAiSite({
14
16
  organizationId = null,
15
17
  organizationWebsite = null,
16
18
  includeDistrictResults = true,
19
+ functionsEndpoint = null,
17
20
  }) {
18
21
  const q = (question || '').trim()
19
22
  if (!q) {
20
23
  return { answer: null, sources: [] }
21
24
  }
22
25
 
26
+ const endpoint = resolveFunctionsEndpoint({ functionsEndpoint })
27
+ if (!endpoint) {
28
+ throw new Error('functionsEndpoint is required for AI site search.')
29
+ }
30
+
23
31
  const { data } = await axios.post(
24
- process.env.FUNCTIONS_ENDPOINT + '/ai-site-search',
32
+ endpoint.replace(/\/$/, '') + '/ai-site-search',
25
33
  {
26
34
  question: q,
27
35
  organizationId,