@asd20/ui-next 2.11.0 → 2.12.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ # [2.12.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.11.1...ui-next-v2.12.0) (2026-05-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add querypeople to public api contract ([a1a3141](https://github.com/academydistrict20/asd20-ui-next/commit/a1a3141ec8b3df29da27e947fecba80baf0169e1))
9
+
10
+
11
+ ### Features
12
+
13
+ * add people to ai search ([241cf12](https://github.com/academydistrict20/asd20-ui-next/commit/241cf1247633f83a47b6fe3cd3bc91bbff275e80))
14
+
15
+ ## [2.11.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.11.0...ui-next-v2.11.1) (2026-05-26)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * remove search parameters from url ([b6bde4c](https://github.com/academydistrict20/asd20-ui-next/commit/b6bde4cb29ea762267ecd03f9739a47d8726ce1e))
21
+
3
22
  # [2.11.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.10.11...ui-next-v2.11.0) (2026-05-26)
4
23
 
5
24
 
package/helpers.js CHANGED
@@ -42,6 +42,7 @@ export { default as queryEvents } from './src/helpers/queryEvents.js'
42
42
  export { default as queryFiles } from './src/helpers/queryFiles.js'
43
43
  export { default as queryMessages } from './src/helpers/queryMessages.js'
44
44
  export { default as queryPages } from './src/helpers/queryPages.js'
45
+ export { default as queryPeople } from './src/helpers/queryPeople.js'
45
46
  export { default as sendAccessibilityIssue } from './src/helpers/sendAccessibilityIssue.js'
46
47
  export { default as sendEmail } from './src/helpers/sendEmail.js'
47
48
  export { default as sortOrgsByLevel } from './src/helpers/sortOrgsByLevel.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -53,12 +53,14 @@ export default {
53
53
  pages: { type: Array, default: () => [] },
54
54
  files: { type: Array, default: () => [] },
55
55
  groups: { type: Array, default: () => [] },
56
+ people: { type: Array, default: () => [] },
56
57
  showContacts: { type: Boolean, default: true },
57
58
  includeDistrictResults: { type: Boolean, default: true },
58
59
  searchLanguageCode: { type: String, default: null },
59
60
  queryPagesHandler: { type: Function, default: null },
60
61
  queryFilesHandler: { type: Function, default: null },
61
62
  queryGroupsHandler: { type: Function, default: null },
63
+ queryPeopleHandler: { type: Function, default: null },
62
64
  queryAiSiteHandler: { type: Function, default: null },
63
65
  organization: { type: Object, default: () => ({}) },
64
66
  organizationOptions: { type: Array, default: () => [] },
@@ -71,11 +71,13 @@
71
71
  :pages="pages"
72
72
  :files="files"
73
73
  :groups="groups"
74
+ :people="people"
74
75
  :include-district-results="includeDistrictResults"
75
76
  :search-language-code="searchLanguageCode"
76
77
  :query-pages-handler="queryPagesHandler"
77
78
  :query-files-handler="queryFilesHandler"
78
79
  :query-groups-handler="queryGroupsHandler"
80
+ :query-people-handler="queryPeopleHandler"
79
81
  :query-ai-site-handler="queryAiSiteHandler"
80
82
  :organization="organization"
81
83
  :organization-options="organizationOptions"
@@ -118,12 +120,14 @@ export default {
118
120
  pages: { type: Array, default: () => [] },
119
121
  files: { type: Array, default: () => [] },
120
122
  groups: { type: Array, default: () => [] },
123
+ people: { type: Array, default: () => [] },
121
124
  showContacts: { type: Boolean, default: true },
122
125
  includeDistrictResults: { type: Boolean, default: true },
123
126
  searchLanguageCode: { type: String, default: null },
124
127
  queryPagesHandler: { type: Function, default: null },
125
128
  queryFilesHandler: { type: Function, default: null },
126
129
  queryGroupsHandler: { type: Function, default: null },
130
+ queryPeopleHandler: { type: Function, default: null },
127
131
  queryAiSiteHandler: { type: Function, default: null },
128
132
  organization: { type: Object, default: null },
129
133
  organizationOptions: { type: Array, default: () => [] },
@@ -324,37 +324,35 @@
324
324
  v-if="
325
325
  hasSubmitted &&
326
326
  showContacts &&
327
- currentGroups.length === 0 &&
328
- !searchingFiles
327
+ contactResultCount === 0 &&
328
+ !searchingFiles &&
329
+ !searchingPeople
329
330
  "
330
331
  title="No Contacts Found"
331
332
  description="Try using different keywords."
332
333
  />
333
334
  <div
334
- v-if="currentGroups.length > 0 && showContacts"
335
+ v-if="contactResultCount > 0 && showContacts"
335
336
  class="asd20-site-search__suggested"
336
337
  >
337
338
  <h3>
338
339
  {{
339
- currentGroups.length > 1
340
+ contactResultCount > 1
340
341
  ? 'Suggested Contacts:'
341
342
  : 'Suggested Contact:'
342
343
  }}
343
344
  </h3>
344
345
  <asd20-list-item
345
- v-for="item in groupListItems"
346
- :key="item.group.id"
347
- :text-avatar="item.textAvatar"
348
- :label="item.label"
349
- :logo-url="item.logoUrl"
350
- :source-title="item.sourceTitle"
351
- :attribution="!!item.logoUrl"
352
- actionable
353
- @click="selectedGroup = item.group"
346
+ v-for="item in contactListItems"
347
+ v-bind="item"
348
+ :key="item.key"
349
+ @click="onContactClick(item)"
354
350
  />
355
351
  </div>
356
352
  <asd20-loader
357
- v-if="searchingFiles && currentGroups.length === 0"
353
+ v-if="
354
+ (searchingFiles || searchingPeople) && contactResultCount === 0
355
+ "
358
356
  size="lg"
359
357
  />
360
358
  </asd20-viewport>
@@ -467,6 +465,12 @@
467
465
  </asd20-viewport>
468
466
  </asd20-modal>
469
467
 
468
+ <asd20-person-modal
469
+ :person="selectedPerson"
470
+ :open="!!selectedPerson"
471
+ @dismiss="selectedPerson = null"
472
+ />
473
+
470
474
  <asd20-modal
471
475
  :open="improvementFeedbackModalOpen"
472
476
  title="Report Missing or Incorrect Information"
@@ -539,6 +543,7 @@ import Asd20Notification from '../../molecules/Asd20Notification'
539
543
  import Asd20Loader from '../../molecules/Asd20Loader'
540
544
  import Asd20Modal from '../../molecules/Asd20Modal'
541
545
  import Asd20DepartmentContactCard from '../../molecules/Asd20DepartmentContactCard'
546
+ import Asd20PersonModal from '../../organisms/Asd20PersonModal'
542
547
  import Asd20Button from '../../atoms/Asd20Button'
543
548
  import Asd20Icon from '../../atoms/Asd20Icon'
544
549
  import Asd20Spinner from '../../atoms/Asd20Spinner'
@@ -548,9 +553,11 @@ import Asd20Spinner from '../../atoms/Asd20Spinner'
548
553
  import debounce from 'lodash/debounce'
549
554
  import mapPagesToListItems from '../../../helpers/mapPagesToListItems'
550
555
  import mapFilesToListItems from '../../../helpers/mapFilesToListItems'
556
+ import mapPeopleToListItems from '../../../helpers/mapPeopleToListItems'
551
557
  import logSearchAnalytics from '../../../helpers/logSearchAnalytics'
552
558
  import logSearchFeedback from '../../../helpers/logSearchFeedback'
553
559
  import queryAiSite from '../../../helpers/queryAiSite'
560
+ import queryPeople from '../../../helpers/queryPeople'
554
561
  import { NO_PAGE_RESULTS_FALLBACKS } from '../../../helpers/queryPages'
555
562
 
556
563
  // Mixins
@@ -604,6 +611,7 @@ export default {
604
611
  Asd20Loader,
605
612
  Asd20Modal,
606
613
  Asd20DepartmentContactCard,
614
+ Asd20PersonModal,
607
615
  Asd20Button,
608
616
  Asd20Icon,
609
617
  Asd20Spinner,
@@ -625,6 +633,11 @@ export default {
625
633
  { type: Array, default: () => [] },
626
634
  'search'
627
635
  ),
636
+ globalPropMixinFactory(
637
+ 'people',
638
+ { type: Array, default: () => [] },
639
+ 'search'
640
+ ),
628
641
  globalPropMixinFactory(
629
642
  'includeDistrictResults',
630
643
  { type: Boolean, default: true },
@@ -642,6 +655,7 @@ export default {
642
655
  queryPagesHandler: { type: Function, default: null },
643
656
  queryFilesHandler: { type: Function, default: null },
644
657
  queryGroupsHandler: { type: Function, default: null },
658
+ queryPeopleHandler: { type: Function, default: null },
645
659
  queryAiSiteHandler: { type: Function, default: null },
646
660
  organization: {
647
661
  type: Object,
@@ -666,12 +680,15 @@ export default {
666
680
  aiTranscript: [],
667
681
 
668
682
  selectedGroup: null,
683
+ selectedPerson: null,
669
684
  searchingPages: false,
670
685
  searchingFiles: false,
686
+ searchingPeople: false,
671
687
 
672
688
  localSearchPages: null,
673
689
  localSearchFiles: null,
674
690
  localSearchGroups: null,
691
+ localSearchPeople: null,
675
692
 
676
693
  keywordsFromAi: false,
677
694
  aiKeywordsDisplayAnswer: '',
@@ -683,7 +700,6 @@ export default {
683
700
  searchMode: '',
684
701
  routeRestoreInProgress: false,
685
702
  lastRestoredSearchKey: '',
686
- skipNextRouteRestore: false,
687
703
  suppressAutoRestore: false,
688
704
  lastManualTabChangeAt: 0,
689
705
  lastManualTabLabel: '',
@@ -724,6 +740,15 @@ export default {
724
740
  return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
725
741
  },
726
742
 
743
+ currentPeople() {
744
+ if (Array.isArray(this.localSearchPeople)) return this.localSearchPeople
745
+ return Array.isArray(this.resolvedPeople) ? this.resolvedPeople : []
746
+ },
747
+
748
+ contactResultCount() {
749
+ return this.currentGroups.length + this.currentPeople.length
750
+ },
751
+
727
752
  shouldUsePageFallbacks() {
728
753
  const hasSearchText =
729
754
  typeof this.keywords === 'string' && this.keywords.trim()
@@ -829,7 +854,7 @@ export default {
829
854
  if (this.showContacts) {
830
855
  assistantTabs.push({
831
856
  label: 'Contacts',
832
- badge: this.currentGroups.length,
857
+ badge: this.contactResultCount,
833
858
  active: this.currentTab === 'Contacts',
834
859
  })
835
860
  }
@@ -852,7 +877,7 @@ export default {
852
877
  if (this.showContacts) {
853
878
  keywordTabs.push({
854
879
  label: 'Contacts',
855
- badge: this.currentGroups.length,
880
+ badge: this.contactResultCount,
856
881
  active: this.currentTab === 'Contacts',
857
882
  })
858
883
  }
@@ -955,6 +980,8 @@ export default {
955
980
  ((g.shortName || g.title || '').match(/\b([A-Z])/g) || []).join('')
956
981
 
957
982
  return {
983
+ key: `group-${g.id || g.title}`,
984
+ contactType: 'group',
958
985
  group: g,
959
986
  label: `${g.title}`,
960
987
  textAvatar,
@@ -963,6 +990,17 @@ export default {
963
990
  }
964
991
  })
965
992
  },
993
+ peopleListItems() {
994
+ return (mapPeopleToListItems(this.currentPeople) || []).map(item => ({
995
+ ...item,
996
+ key: `person-${item.id || item.email || item.label}`,
997
+ contactType: 'person',
998
+ }))
999
+ },
1000
+
1001
+ contactListItems() {
1002
+ return [...this.peopleListItems, ...this.groupListItems]
1003
+ },
966
1004
  feedbackFormUrlRaw() {
967
1005
  if (this.aiSearchFeedbackFormUrl) return this.aiSearchFeedbackFormUrl
968
1006
  const runtimeConfig = this.resolveRuntimeConfig()
@@ -1077,9 +1115,9 @@ export default {
1077
1115
  this.focusQuery()
1078
1116
  this.refreshResultsScrollLayer()
1079
1117
  })
1080
- this.restoreSearchStateFromRoute()
1118
+ this.restoreSearchState()
1081
1119
  } else {
1082
- this.clearSearchStateFromRouteQuery()
1120
+ this.clearLegacySearchRouteQuery()
1083
1121
  if (!this.inline) this.unlockBodyScroll()
1084
1122
  this.blurQuery()
1085
1123
  }
@@ -1090,7 +1128,7 @@ export default {
1090
1128
  if (newVal === 'Answers') {
1091
1129
  this.$nextTick(() => this.refreshResultsScrollLayer())
1092
1130
  }
1093
- this.syncSearchStateToRoute({ tab: newVal })
1131
+ this.syncSearchState({ tab: newVal })
1094
1132
  },
1095
1133
 
1096
1134
  aiAnswer() {
@@ -1115,7 +1153,7 @@ export default {
1115
1153
  this.selectedGroup = null
1116
1154
  this.searchPages({ runId })
1117
1155
  this.searchFilesWithGroupOwners({ runId })
1118
- this.syncSearchStateToRoute()
1156
+ this.syncSearchState()
1119
1157
  }
1120
1158
  }, 400),
1121
1159
 
@@ -1124,7 +1162,7 @@ export default {
1124
1162
  const runId = this.beginSearchRun()
1125
1163
  this.searchPages({ runId })
1126
1164
  this.searchFilesWithGroupOwners({ runId })
1127
- this.syncSearchStateToRoute()
1165
+ this.syncSearchState()
1128
1166
  },
1129
1167
  // Clear AI answer if user clears the input
1130
1168
 
@@ -1137,34 +1175,20 @@ export default {
1137
1175
  this.aiKeywordsDisplayPages = ''
1138
1176
  }
1139
1177
  },
1140
-
1141
- '$route.query': {
1142
- handler() {
1143
- if (this.suppressAutoRestore) return
1144
- if (this.searchingAi) return
1145
- if (this.hasRecentManualTabSelection()) return
1146
- if (this.skipNextRouteRestore) {
1147
- this.skipNextRouteRestore = false
1148
- return
1149
- }
1150
- if (!this.getRouteSearchState(this.$route.query)) {
1151
- this.lastRestoredSearchKey = ''
1152
- }
1153
- if (this.active) this.restoreSearchStateFromRoute()
1154
- },
1155
- },
1156
1178
  },
1157
1179
 
1158
1180
  mounted() {
1159
1181
  if (this.active && !this.inline) {
1160
1182
  this.lockBodyScroll()
1161
1183
  }
1162
- if (this.didPageReload()) {
1184
+ const didReload = this.didPageReload()
1185
+ if (didReload) {
1163
1186
  this.persistSearchState(null)
1164
1187
  this.lastRestoredSearchKey = ''
1165
- this.clearSearchStateFromRouteQuery()
1166
- } else if (this.active) {
1167
- this.restoreSearchStateFromRoute()
1188
+ }
1189
+ this.clearLegacySearchRouteQuery()
1190
+ if (!didReload && this.active) {
1191
+ this.restoreSearchState()
1168
1192
  }
1169
1193
  if (typeof window !== 'undefined') {
1170
1194
  window.addEventListener('resize', this.onWindowReflow, { passive: true })
@@ -1231,7 +1255,7 @@ export default {
1231
1255
  : 'en'
1232
1256
  },
1233
1257
 
1234
- setSearchResults({ pages, files, groups } = {}) {
1258
+ setSearchResults({ pages, files, groups, people } = {}) {
1235
1259
  if (pages !== undefined) {
1236
1260
  this.localSearchPages = Array.isArray(pages) ? pages : []
1237
1261
  }
@@ -1241,12 +1265,16 @@ export default {
1241
1265
  if (groups !== undefined) {
1242
1266
  this.localSearchGroups = Array.isArray(groups) ? groups : []
1243
1267
  }
1268
+ if (people !== undefined) {
1269
+ this.localSearchPeople = Array.isArray(people) ? people : []
1270
+ }
1244
1271
  },
1245
1272
 
1246
1273
  readSearchResults(target) {
1247
1274
  if (target === 'pages') return this.currentPages
1248
1275
  if (target === 'files') return this.currentFiles
1249
1276
  if (target === 'groups') return this.currentGroups
1277
+ if (target === 'people') return this.currentPeople
1250
1278
  return []
1251
1279
  },
1252
1280
 
@@ -1260,6 +1288,9 @@ export default {
1260
1288
  if (target === 'groups') {
1261
1289
  return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
1262
1290
  }
1291
+ if (target === 'people') {
1292
+ return Array.isArray(this.resolvedPeople) ? this.resolvedPeople : []
1293
+ }
1263
1294
  return []
1264
1295
  },
1265
1296
 
@@ -1323,9 +1354,24 @@ export default {
1323
1354
  }
1324
1355
  },
1325
1356
 
1326
- normalizeRouteQueryValue(value) {
1327
- if (Array.isArray(value)) return value[0]
1328
- return value
1357
+ async queryPeopleWithFallback(payload) {
1358
+ if (typeof this.queryPeopleHandler === 'function') {
1359
+ const result = await this.queryPeopleHandler(payload)
1360
+ if (Array.isArray(result)) return result
1361
+ return result && Array.isArray(result.people) ? result.people : []
1362
+ }
1363
+
1364
+ const searchStore = this.getSearchStore()
1365
+ if (searchStore) {
1366
+ await searchStore.dispatch('search/queryPeople', payload)
1367
+ return Array.isArray(this.resolvedPeople) ? this.resolvedPeople : []
1368
+ }
1369
+
1370
+ const { people } = await queryPeople({
1371
+ ...payload,
1372
+ runtimeConfig: this.resolveRuntimeConfig(),
1373
+ })
1374
+ return Array.isArray(people) ? people : []
1329
1375
  },
1330
1376
 
1331
1377
  isLocalDevelopmentHost(hostname = '') {
@@ -1406,7 +1452,7 @@ export default {
1406
1452
  if (this.searchingAi) return
1407
1453
  if (this.hasRecentManualTabSelection()) return
1408
1454
  if (!this.active || this.routeRestoreInProgress) return
1409
- this.restoreSearchStateFromRoute()
1455
+ this.restoreSearchState()
1410
1456
  },
1411
1457
 
1412
1458
  hasRecentManualTabSelection() {
@@ -1546,6 +1592,7 @@ export default {
1546
1592
  pages: Array.isArray(this.currentPages) ? this.currentPages : [],
1547
1593
  files: Array.isArray(this.currentFiles) ? this.currentFiles : [],
1548
1594
  groups: Array.isArray(this.currentGroups) ? this.currentGroups : [],
1595
+ people: Array.isArray(this.currentPeople) ? this.currentPeople : [],
1549
1596
  aiAnswer: this.aiAnswer || null,
1550
1597
  aiSources: Array.isArray(this.aiSources) ? this.aiSources : [],
1551
1598
  aiErrorMessage: this.aiErrorMessage || '',
@@ -1560,7 +1607,7 @@ export default {
1560
1607
  },
1561
1608
 
1562
1609
  persistSearchCacheSnapshot(overrides = {}) {
1563
- const state = this.getCurrentSearchStateForRoute(overrides)
1610
+ const state = this.getCurrentSearchState(overrides)
1564
1611
  if (!state) return
1565
1612
  this.persistSearchState(state, {
1566
1613
  cache: this.buildSearchCacheSnapshot(),
@@ -1656,7 +1703,8 @@ export default {
1656
1703
  const pages = Array.isArray(snapshot.pages) ? snapshot.pages : []
1657
1704
  const files = Array.isArray(snapshot.files) ? snapshot.files : []
1658
1705
  const groups = Array.isArray(snapshot.groups) ? snapshot.groups : []
1659
- this.setSearchResults({ pages, files, groups })
1706
+ const people = Array.isArray(snapshot.people) ? snapshot.people : []
1707
+ this.setSearchResults({ pages, files, groups, people })
1660
1708
 
1661
1709
  if (!skipTabUpdate) {
1662
1710
  const preferredTab = SEARCH_ROUTE_TABS.has(targetState.tab)
@@ -1673,33 +1721,7 @@ export default {
1673
1721
  return true
1674
1722
  },
1675
1723
 
1676
- getRouteSearchState(routeQuery = {}) {
1677
- const rawText = this.normalizeRouteQueryValue(
1678
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.text]
1679
- )
1680
- const rawIncludeDistrictResults = this.normalizeRouteQueryValue(
1681
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.includeDistrictResults]
1682
- )
1683
- const rawMode = this.normalizeRouteQueryValue(
1684
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.mode]
1685
- )
1686
- const rawTab = this.normalizeRouteQueryValue(
1687
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.tab]
1688
- )
1689
- const rawUpdatedAt = this.normalizeRouteQueryValue(
1690
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.updatedAt]
1691
- )
1692
-
1693
- return this.normalizeSearchState({
1694
- text: rawText,
1695
- mode: rawMode,
1696
- tab: rawTab,
1697
- includeDistrictResults: rawIncludeDistrictResults,
1698
- updatedAt: rawUpdatedAt,
1699
- })
1700
- },
1701
-
1702
- getCurrentSearchStateForRoute(overrides = {}) {
1724
+ getCurrentSearchState(overrides = {}) {
1703
1725
  const hasSubmitted =
1704
1726
  typeof overrides.hasSubmitted === 'boolean'
1705
1727
  ? overrides.hasSubmitted
@@ -1746,35 +1768,17 @@ export default {
1746
1768
  }
1747
1769
  },
1748
1770
 
1749
- buildRouteSearchQueryValues(state) {
1750
- if (!state) return {}
1751
- const values = {
1752
- [SEARCH_ROUTE_QUERY_KEYS.text]: state.text,
1753
- [SEARCH_ROUTE_QUERY_KEYS.mode]: state.mode,
1754
- [SEARCH_ROUTE_QUERY_KEYS.tab]: state.tab,
1755
- [SEARCH_ROUTE_QUERY_KEYS.includeDistrictResults]:
1756
- state.includeDistrictResults ? '1' : '0',
1757
- }
1758
- const updatedAt = Number(state.updatedAt)
1759
- if (Number.isFinite(updatedAt) && updatedAt > 0) {
1760
- values[SEARCH_ROUTE_QUERY_KEYS.updatedAt] = String(
1761
- Math.round(updatedAt)
1762
- )
1763
- }
1764
- return values
1765
- },
1766
-
1767
- buildQueryWithSearchState(state) {
1771
+ buildQueryWithoutSearchState() {
1768
1772
  const nextQuery = { ...((this.$route && this.$route.query) || {}) }
1769
1773
  Object.keys(SEARCH_ROUTE_QUERY_KEYS).forEach(key => {
1770
1774
  delete nextQuery[SEARCH_ROUTE_QUERY_KEYS[key]]
1771
1775
  })
1772
- return { ...nextQuery, ...this.buildRouteSearchQueryValues(state) }
1776
+ return nextQuery
1773
1777
  },
1774
1778
 
1775
- async clearSearchStateFromRouteQuery() {
1779
+ async clearLegacySearchRouteQuery() {
1776
1780
  if (!this.$router || !this.$route) return
1777
- const nextQuery = this.buildQueryWithSearchState(null)
1781
+ const nextQuery = this.buildQueryWithoutSearchState()
1778
1782
  await this.replaceRouteQuery(nextQuery)
1779
1783
  },
1780
1784
 
@@ -1800,25 +1804,20 @@ export default {
1800
1804
  async replaceRouteQuery(query) {
1801
1805
  if (!this.$router || !this.$route) return
1802
1806
  if (this.isSameQuery(this.$route.query, query)) return
1803
- this.skipNextRouteRestore = true
1804
1807
  try {
1805
1808
  await this.$router.replace({ path: this.$route.path, query })
1806
1809
  } catch (error) {
1807
- this.skipNextRouteRestore = false
1808
1810
  // Ignore duplicate navigation and transient route replacement races.
1809
1811
  }
1810
1812
  },
1811
1813
 
1812
- async syncSearchStateToRoute(overrides = {}) {
1814
+ syncSearchState(overrides = {}) {
1813
1815
  if (this.routeRestoreInProgress) return
1814
- if (!this.$router || !this.$route) return
1815
- const state = this.getCurrentSearchStateForRoute(overrides)
1816
+ const state = this.getCurrentSearchState(overrides)
1816
1817
  this.lastRestoredSearchKey = state
1817
- ? `route:${JSON.stringify(state)}`
1818
+ ? `storage:${JSON.stringify(state)}`
1818
1819
  : ''
1819
1820
  this.persistSearchState(state, { preserveCache: true })
1820
- const nextQuery = this.buildQueryWithSearchState(state)
1821
- await this.replaceRouteQuery(nextQuery)
1822
1821
  },
1823
1822
 
1824
1823
  appendSearchContextToUrl(rawUrl) {
@@ -1826,14 +1825,11 @@ export default {
1826
1825
  return rawUrl
1827
1826
  },
1828
1827
 
1829
- async restoreSearchStateFromRoute() {
1828
+ async restoreSearchState() {
1830
1829
  if (this.suppressAutoRestore) return
1831
1830
  if (this.searchingAi) return
1832
- if (!this.$route || this.routeRestoreInProgress) return
1831
+ if (this.routeRestoreInProgress) return
1833
1832
  const restoreStartedAt = Date.now()
1834
- const routeStateCandidate = this.getRouteSearchState(this.$route.query)
1835
- const routeStateFresh = this.isSearchStateFresh(routeStateCandidate)
1836
- const routeState = routeStateFresh ? routeStateCandidate : null
1837
1833
  const persistedRecordCandidate = this.readPersistedSearchRecord()
1838
1834
  const persistedRecordFresh =
1839
1835
  persistedRecordCandidate &&
@@ -1845,24 +1841,16 @@ export default {
1845
1841
  if (persistedRecordCandidate && !persistedRecordFresh) {
1846
1842
  this.persistSearchState(null)
1847
1843
  }
1848
- if (routeStateCandidate && !routeStateFresh) {
1849
- await this.clearSearchStateFromRouteQuery()
1850
- }
1851
1844
 
1852
- const persistedState = routeState
1853
- ? null
1854
- : persistedRecord && persistedRecord.state
1855
- ? persistedRecord.state
1856
- : null
1857
- const restoredState = routeState || persistedState
1845
+ const restoredState =
1846
+ persistedRecord && persistedRecord.state ? persistedRecord.state : null
1858
1847
  if (!restoredState) return
1859
1848
 
1860
- const restoreSource = routeState ? 'route' : 'storage'
1861
1849
  const cacheVersion = this.getSearchCacheVersion(
1862
1850
  persistedRecord,
1863
1851
  restoredState
1864
1852
  )
1865
- const restoreKey = `${restoreSource}:${JSON.stringify(
1853
+ const restoreKey = `storage:${JSON.stringify(
1866
1854
  restoredState
1867
1855
  )}:${cacheVersion}`
1868
1856
  if (restoreKey === this.lastRestoredSearchKey) return
@@ -2253,10 +2241,43 @@ export default {
2253
2241
 
2254
2242
  dismiss() {
2255
2243
  if (!this.active) return
2256
- this.clearSearchStateFromRouteQuery()
2244
+ this.clearLegacySearchRouteQuery()
2257
2245
  this.$emit('update:active', false)
2258
2246
  },
2259
2247
 
2248
+ onContactClick(item) {
2249
+ if (!item) return
2250
+ if (item.contactType === 'person') {
2251
+ this.selectedPerson = this.normalizePersonForModal(item.person || item)
2252
+ return
2253
+ }
2254
+ if (item.group) {
2255
+ this.selectedGroup = item.group
2256
+ }
2257
+ },
2258
+
2259
+ normalizePersonForModal(person = {}) {
2260
+ const jobTitles = Array.isArray(person.jobTitles)
2261
+ ? person.jobTitles
2262
+ : person.jobTitles
2263
+ ? [person.jobTitles]
2264
+ : person.jobTypes
2265
+ ? Array.isArray(person.jobTypes)
2266
+ ? person.jobTypes
2267
+ : [person.jobTypes]
2268
+ : []
2269
+
2270
+ return {
2271
+ ...person,
2272
+ id: person.id || person.employeeId || person.profileId || person.userId,
2273
+ displayName: person.displayName || person.name || '',
2274
+ jobTitles,
2275
+ workPhone: person.workPhone || person.phone || '',
2276
+ thumbnailUrl:
2277
+ person.thumbnailUrl || person.thumbnail || person.photoUrl || '',
2278
+ }
2279
+ },
2280
+
2260
2281
  resolveRuntimeConfig() {
2261
2282
  const runtimeConfig =
2262
2283
  this.$config ||
@@ -2421,9 +2442,11 @@ export default {
2421
2442
  this.beginSearchRun()
2422
2443
  }
2423
2444
  this.selectedGroup = null
2445
+ this.selectedPerson = null
2424
2446
  this.searchingAi = preserveAiLoading ? this.searchingAi : false
2425
2447
  this.searchingPages = false
2426
2448
  this.searchingFiles = false
2449
+ this.searchingPeople = false
2427
2450
  this.activeAiQuestionKey = preserveAiLoading
2428
2451
  ? this.activeAiQuestionKey
2429
2452
  : ''
@@ -2446,7 +2469,7 @@ export default {
2446
2469
  this.aiTranscript = []
2447
2470
  this.anchoredUserTurnId = null
2448
2471
  }
2449
- this.setSearchResults({ pages: [], files: [], groups: [] })
2472
+ this.setSearchResults({ pages: [], files: [], groups: [], people: [] })
2450
2473
  },
2451
2474
 
2452
2475
  async clearInput() {
@@ -2460,7 +2483,7 @@ export default {
2460
2483
  await this.clearSearchState()
2461
2484
  this.currentTab = 'Answers'
2462
2485
  this.persistSearchState(null)
2463
- await this.syncSearchStateToRoute({ hasSubmitted: false, text: '' })
2486
+ this.syncSearchState({ hasSubmitted: false, text: '' })
2464
2487
  this.lastRestoredSearchKey = ''
2465
2488
 
2466
2489
  this.$nextTick(() => {
@@ -2551,8 +2574,11 @@ export default {
2551
2574
  async searchFilesWithGroupOwners(options = {}) {
2552
2575
  const runId = options.runId || this.searchRunId
2553
2576
  const groupsPromise = this.searchGroups({ runId })
2577
+ const peoplePromise = this.searchPeople({ runId })
2554
2578
  await this.searchFiles({ runId })
2555
2579
  if (!this.isCurrentSearchRun(runId)) return []
2580
+ await peoplePromise
2581
+ if (!this.isCurrentSearchRun(runId)) return []
2556
2582
  const ownerFilters = await groupsPromise
2557
2583
  if (!this.isCurrentSearchRun(runId)) return ownerFilters
2558
2584
  if (ownerFilters.length > 0) {
@@ -2593,6 +2619,32 @@ export default {
2593
2619
  )
2594
2620
  },
2595
2621
 
2622
+ async searchPeople(options = {}) {
2623
+ const runId = options.runId || this.searchRunId
2624
+ const searchInput = {
2625
+ keywords: this.keywords,
2626
+ question: this.searchQuestion || this.inputText,
2627
+ }
2628
+
2629
+ this.searchingPeople = true
2630
+ try {
2631
+ let people = []
2632
+ try {
2633
+ people = await this.queryPeopleWithFallback(searchInput)
2634
+ } catch (error) {
2635
+ people = []
2636
+ }
2637
+ if (!this.isCurrentSearchRun(runId)) return this.currentPeople
2638
+ this.setSearchResults({ people })
2639
+ this.persistSearchCacheSnapshot()
2640
+ return this.currentPeople
2641
+ } finally {
2642
+ if (this.isCurrentSearchRun(runId)) {
2643
+ this.searchingPeople = false
2644
+ }
2645
+ }
2646
+ },
2647
+
2596
2648
  async onSubmitInput() {
2597
2649
  if (this.searchingAi) return
2598
2650
  const text = (this.inputText || '').trim()
@@ -2602,7 +2654,7 @@ export default {
2602
2654
  this.searchMode = ''
2603
2655
  await this.clearSearchState()
2604
2656
  this.persistSearchState(null)
2605
- await this.syncSearchStateToRoute({ hasSubmitted: false, text: '' })
2657
+ this.syncSearchState({ hasSubmitted: false, text: '' })
2606
2658
  this.lastRestoredSearchKey = ''
2607
2659
  this.$nextTick(() => {
2608
2660
  this.suppressAutoRestore = false
@@ -2617,7 +2669,7 @@ export default {
2617
2669
  this.searchMode = mode
2618
2670
 
2619
2671
  if (mode === 'question') {
2620
- this.syncSearchStateToRoute({ text, mode, tab: 'Answers' })
2672
+ this.syncSearchState({ text, mode, tab: 'Answers' })
2621
2673
  this.searchAi(text)
2622
2674
  } else {
2623
2675
  await this.clearSearchState()
@@ -2629,7 +2681,7 @@ export default {
2629
2681
  this.searchFilesWithGroupOwners({ runId })
2630
2682
  if (!this.isCurrentSearchRun(runId)) return
2631
2683
  this.currentTab = 'Pages'
2632
- this.syncSearchStateToRoute({ text, mode, tab: 'Pages' })
2684
+ this.syncSearchState({ text, mode, tab: 'Pages' })
2633
2685
  this.$nextTick(() => this.refreshResultsScrollLayer())
2634
2686
 
2635
2687
  // Log keyword-only search analytics
@@ -13,11 +13,13 @@
13
13
  :pages="searchPages"
14
14
  :files="searchFiles"
15
15
  :groups="searchGroups"
16
+ :people="searchPeople"
16
17
  :include-district-results="includeDistrictResults"
17
18
  :search-language-code="searchLanguageCode"
18
19
  :query-pages-handler="queryPagesHandler"
19
20
  :query-files-handler="queryFilesHandler"
20
21
  :query-groups-handler="queryGroupsHandler"
22
+ :query-people-handler="queryPeopleHandler"
21
23
  :query-ai-site-handler="queryAiSiteHandler"
22
24
  :organization="organization"
23
25
  :organization-options="organizationOptions"
@@ -65,11 +67,13 @@
65
67
  :pages="searchPages"
66
68
  :files="searchFiles"
67
69
  :groups="searchGroups"
70
+ :people="searchPeople"
68
71
  :include-district-results="includeDistrictResults"
69
72
  :search-language-code="searchLanguageCode"
70
73
  :query-pages-handler="queryPagesHandler"
71
74
  :query-files-handler="queryFilesHandler"
72
75
  :query-groups-handler="queryGroupsHandler"
76
+ :query-people-handler="queryPeopleHandler"
73
77
  :query-ai-site-handler="queryAiSiteHandler"
74
78
  :organization="organization"
75
79
  :organization-options="organizationOptions"
@@ -221,11 +225,13 @@ export default {
221
225
  searchPages: { type: Array, default: () => [] },
222
226
  searchFiles: { type: Array, default: () => [] },
223
227
  searchGroups: { type: Array, default: () => [] },
228
+ searchPeople: { type: Array, default: () => [] },
224
229
  includeDistrictResults: { type: Boolean, default: true },
225
230
  searchLanguageCode: { type: String, default: null },
226
231
  queryPagesHandler: { type: Function, default: null },
227
232
  queryFilesHandler: { type: Function, default: null },
228
233
  queryGroupsHandler: { type: Function, default: null },
234
+ queryPeopleHandler: { type: Function, default: null },
229
235
  queryAiSiteHandler: { type: Function, default: null },
230
236
  aiSearchFeedbackFormUrl: { type: String, default: '' },
231
237
  },
@@ -13,11 +13,13 @@
13
13
  :pages="searchPages"
14
14
  :files="searchFiles"
15
15
  :groups="searchGroups"
16
+ :people="searchPeople"
16
17
  :include-district-results="includeDistrictResults"
17
18
  :search-language-code="searchLanguageCode"
18
19
  :query-pages-handler="queryPagesHandler"
19
20
  :query-files-handler="queryFilesHandler"
20
21
  :query-groups-handler="queryGroupsHandler"
22
+ :query-people-handler="queryPeopleHandler"
21
23
  :query-ai-site-handler="queryAiSiteHandler"
22
24
  :organization-options="organizationOptions"
23
25
  :organization="organization"
@@ -109,11 +111,13 @@
109
111
  :pages="searchPages"
110
112
  :files="searchFiles"
111
113
  :groups="searchGroups"
114
+ :people="searchPeople"
112
115
  :include-district-results="includeDistrictResults"
113
116
  :search-language-code="searchLanguageCode"
114
117
  :query-pages-handler="queryPagesHandler"
115
118
  :query-files-handler="queryFilesHandler"
116
119
  :query-groups-handler="queryGroupsHandler"
120
+ :query-people-handler="queryPeopleHandler"
117
121
  :query-ai-site-handler="queryAiSiteHandler"
118
122
  :organization="organization"
119
123
  :organization-options="organizationOptions"
@@ -234,11 +238,13 @@ export default {
234
238
  searchPages: { type: Array, default: () => [] },
235
239
  searchFiles: { type: Array, default: () => [] },
236
240
  searchGroups: { type: Array, default: () => [] },
241
+ searchPeople: { type: Array, default: () => [] },
237
242
  includeDistrictResults: { type: Boolean, default: true },
238
243
  searchLanguageCode: { type: String, default: null },
239
244
  queryPagesHandler: { type: Function, default: null },
240
245
  queryFilesHandler: { type: Function, default: null },
241
246
  queryGroupsHandler: { type: Function, default: null },
247
+ queryPeopleHandler: { type: Function, default: null },
242
248
  queryAiSiteHandler: { type: Function, default: null },
243
249
  aiSearchFeedbackFormUrl: { type: String, default: '' },
244
250
  },
@@ -13,11 +13,13 @@
13
13
  :pages="searchPages"
14
14
  :files="searchFiles"
15
15
  :groups="searchGroups"
16
+ :people="searchPeople"
16
17
  :include-district-results="includeDistrictResults"
17
18
  :search-language-code="searchLanguageCode"
18
19
  :query-pages-handler="queryPagesHandler"
19
20
  :query-files-handler="queryFilesHandler"
20
21
  :query-groups-handler="queryGroupsHandler"
22
+ :query-people-handler="queryPeopleHandler"
21
23
  :query-ai-site-handler="queryAiSiteHandler"
22
24
  :organization="organization"
23
25
  :organization-options="organizationOptions"
@@ -110,11 +112,13 @@
110
112
  :pages="searchPages"
111
113
  :files="searchFiles"
112
114
  :groups="searchGroups"
115
+ :people="searchPeople"
113
116
  :include-district-results="includeDistrictResults"
114
117
  :search-language-code="searchLanguageCode"
115
118
  :query-pages-handler="queryPagesHandler"
116
119
  :query-files-handler="queryFilesHandler"
117
120
  :query-groups-handler="queryGroupsHandler"
121
+ :query-people-handler="queryPeopleHandler"
118
122
  :query-ai-site-handler="queryAiSiteHandler"
119
123
  :organization="organization"
120
124
  :organization-options="organizationOptions"
@@ -239,11 +243,13 @@ export default {
239
243
  searchPages: { type: Array, default: () => [] },
240
244
  searchFiles: { type: Array, default: () => [] },
241
245
  searchGroups: { type: Array, default: () => [] },
246
+ searchPeople: { type: Array, default: () => [] },
242
247
  includeDistrictResults: { type: Boolean, default: true },
243
248
  searchLanguageCode: { type: String, default: null },
244
249
  queryPagesHandler: { type: Function, default: null },
245
250
  queryFilesHandler: { type: Function, default: null },
246
251
  queryGroupsHandler: { type: Function, default: null },
252
+ queryPeopleHandler: { type: Function, default: null },
247
253
  queryAiSiteHandler: { type: Function, default: null },
248
254
  aiSearchFeedbackFormUrl: { type: String, default: '' },
249
255
  },
@@ -0,0 +1,122 @@
1
+ import axios from 'axios'
2
+
3
+ import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
5
+
6
+ function searchPayload({ keywords = '', top = 10 }) {
7
+ const trimmedKeywords = keywords.trim()
8
+ const terms = trimmedKeywords.split(/\s+/).filter(Boolean)
9
+ const expandedTerms = terms
10
+ .map(term => `displayName:${term}* OR email:${term}*`)
11
+ .join(' OR ')
12
+
13
+ return {
14
+ count: true,
15
+ queryType: 'full',
16
+ search: trimmedKeywords
17
+ ? `"${trimmedKeywords}"${expandedTerms ? ` OR ${expandedTerms}` : ''}`
18
+ : '*',
19
+ top,
20
+ }
21
+ }
22
+
23
+ export default async function queryPeople({
24
+ keywords = '',
25
+ top = 10,
26
+ limit = 0,
27
+ requireKeywords = true,
28
+ indexName = 'people-index',
29
+ requestUrl = '',
30
+ runtimeConfig = null,
31
+ searchKey = '',
32
+ apiVersion = '',
33
+ searchEndpoint = '',
34
+ endpoint = '',
35
+ }) {
36
+ if (!keywords && requireKeywords) {
37
+ return {
38
+ people: [],
39
+ facets: [],
40
+ }
41
+ }
42
+
43
+ const resolvedLimit = Number.isFinite(Number(limit)) && Number(limit) > 0
44
+ ? Number(limit)
45
+ : top
46
+ const resolvedSearchKey = resolveRuntimeConfigValue({
47
+ explicitValue: searchKey,
48
+ runtimeConfig,
49
+ runtimeConfigKeys: ['azureSearchKey'],
50
+ envKeys: [
51
+ 'AZURE_SEARCH_KEY',
52
+ 'VUE_APP_AZURE_SEARCH_KEY',
53
+ 'GRIDSOME_AZURE_SEARCH_KEY',
54
+ ],
55
+ })
56
+ const resolvedApiVersion = resolveRuntimeConfigValue({
57
+ explicitValue: apiVersion,
58
+ runtimeConfig,
59
+ runtimeConfigKeys: ['azureSearchVersion'],
60
+ envKeys: [
61
+ 'AZURE_SEARCH_VERSION',
62
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
63
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
64
+ ],
65
+ })
66
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
67
+ explicitValue: searchEndpoint || endpoint,
68
+ runtimeConfig,
69
+ runtimeConfigKeys: ['azureSearchEndpoint'],
70
+ envKeys: [
71
+ 'AZURE_SEARCH_ENDPOINT',
72
+ 'VUE_APP_AZURE_SEARCH_ENDPOINT',
73
+ 'GRIDSOME_AZURE_SEARCH_ENDPOINT',
74
+ ],
75
+ })
76
+ const resolvedRequestUrl =
77
+ typeof requestUrl === 'string' && requestUrl.trim()
78
+ ? requestUrl.trim()
79
+ : !resolvedSearchEndpoint &&
80
+ hasSearchProxyRuntime(runtimeConfig)
81
+ ? resolveSearchProxyUrl('people', runtimeConfig)
82
+ : ''
83
+
84
+ if (
85
+ !resolvedRequestUrl &&
86
+ (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
87
+ ) {
88
+ throw new Error('Azure Search or proxy configuration is required for queryPeople.')
89
+ }
90
+
91
+ const payload = {
92
+ keywords,
93
+ limit: resolvedLimit,
94
+ indexName,
95
+ }
96
+
97
+ const { data } = resolvedRequestUrl
98
+ ? await axios({
99
+ method: 'post',
100
+ url: resolvedRequestUrl,
101
+ data: payload,
102
+ })
103
+ : await axios({
104
+ method: 'post',
105
+ headers: {
106
+ 'api-key': resolvedSearchKey,
107
+ },
108
+ params: {
109
+ 'api-version': resolvedApiVersion,
110
+ },
111
+ url: `${normalizeBaseUrl(resolvedSearchEndpoint)}/indexes/${indexName}/docs/search`,
112
+ data: searchPayload({
113
+ keywords,
114
+ top: resolvedLimit,
115
+ }),
116
+ })
117
+
118
+ return {
119
+ people: Array.isArray(data.value) ? data.value : [],
120
+ facets: data['@search.facets'],
121
+ }
122
+ }
@@ -5,6 +5,7 @@ const SEARCH_PROXY_PATHS = Object.freeze({
5
5
  pages: '/api/search/pages',
6
6
  files: '/api/search/files',
7
7
  events: '/api/search/events',
8
+ people: '/api/search/people',
8
9
  })
9
10
 
10
11
  export function resolveSearchProxyPath(kind) {