@asd20/ui-next 2.11.1 → 2.12.1

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.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.12.0...ui-next-v2.12.1) (2026-05-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add group matches to people searches by keyword ([1edabb4](https://github.com/academydistrict20/asd20-ui-next/commit/1edabb458ead8430e04375cdb00071db345605f3))
9
+
10
+ # [2.12.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.11.1...ui-next-v2.12.0) (2026-05-26)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * add querypeople to public api contract ([a1a3141](https://github.com/academydistrict20/asd20-ui-next/commit/a1a3141ec8b3df29da27e947fecba80baf0169e1))
16
+
17
+
18
+ ### Features
19
+
20
+ * add people to ai search ([241cf12](https://github.com/academydistrict20/asd20-ui-next/commit/241cf1247633f83a47b6fe3cd3bc91bbff275e80))
21
+
3
22
  ## [2.11.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.11.0...ui-next-v2.11.1) (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.1",
3
+ "version": "2.12.1",
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: '',
@@ -723,6 +740,15 @@ export default {
723
740
  return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
724
741
  },
725
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
+
726
752
  shouldUsePageFallbacks() {
727
753
  const hasSearchText =
728
754
  typeof this.keywords === 'string' && this.keywords.trim()
@@ -828,7 +854,7 @@ export default {
828
854
  if (this.showContacts) {
829
855
  assistantTabs.push({
830
856
  label: 'Contacts',
831
- badge: this.currentGroups.length,
857
+ badge: this.contactResultCount,
832
858
  active: this.currentTab === 'Contacts',
833
859
  })
834
860
  }
@@ -851,7 +877,7 @@ export default {
851
877
  if (this.showContacts) {
852
878
  keywordTabs.push({
853
879
  label: 'Contacts',
854
- badge: this.currentGroups.length,
880
+ badge: this.contactResultCount,
855
881
  active: this.currentTab === 'Contacts',
856
882
  })
857
883
  }
@@ -954,6 +980,8 @@ export default {
954
980
  ((g.shortName || g.title || '').match(/\b([A-Z])/g) || []).join('')
955
981
 
956
982
  return {
983
+ key: `group-${g.id || g.title}`,
984
+ contactType: 'group',
957
985
  group: g,
958
986
  label: `${g.title}`,
959
987
  textAvatar,
@@ -962,6 +990,17 @@ export default {
962
990
  }
963
991
  })
964
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
+ },
965
1004
  feedbackFormUrlRaw() {
966
1005
  if (this.aiSearchFeedbackFormUrl) return this.aiSearchFeedbackFormUrl
967
1006
  const runtimeConfig = this.resolveRuntimeConfig()
@@ -1216,7 +1255,7 @@ export default {
1216
1255
  : 'en'
1217
1256
  },
1218
1257
 
1219
- setSearchResults({ pages, files, groups } = {}) {
1258
+ setSearchResults({ pages, files, groups, people } = {}) {
1220
1259
  if (pages !== undefined) {
1221
1260
  this.localSearchPages = Array.isArray(pages) ? pages : []
1222
1261
  }
@@ -1226,12 +1265,16 @@ export default {
1226
1265
  if (groups !== undefined) {
1227
1266
  this.localSearchGroups = Array.isArray(groups) ? groups : []
1228
1267
  }
1268
+ if (people !== undefined) {
1269
+ this.localSearchPeople = Array.isArray(people) ? people : []
1270
+ }
1229
1271
  },
1230
1272
 
1231
1273
  readSearchResults(target) {
1232
1274
  if (target === 'pages') return this.currentPages
1233
1275
  if (target === 'files') return this.currentFiles
1234
1276
  if (target === 'groups') return this.currentGroups
1277
+ if (target === 'people') return this.currentPeople
1235
1278
  return []
1236
1279
  },
1237
1280
 
@@ -1245,6 +1288,9 @@ export default {
1245
1288
  if (target === 'groups') {
1246
1289
  return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
1247
1290
  }
1291
+ if (target === 'people') {
1292
+ return Array.isArray(this.resolvedPeople) ? this.resolvedPeople : []
1293
+ }
1248
1294
  return []
1249
1295
  },
1250
1296
 
@@ -1308,6 +1354,83 @@ export default {
1308
1354
  }
1309
1355
  },
1310
1356
 
1357
+ async queryPeopleWithFallback(payload) {
1358
+ if (typeof this.queryPeopleHandler === 'function') {
1359
+ const result = await this.queryPeopleHandler(payload)
1360
+ if (Array.isArray(result)) {
1361
+ return { people: result, groups: [] }
1362
+ }
1363
+ return {
1364
+ people: result && Array.isArray(result.people) ? result.people : [],
1365
+ groups: result && Array.isArray(result.groups) ? result.groups : [],
1366
+ }
1367
+ }
1368
+
1369
+ const searchStore = this.getSearchStore()
1370
+ if (searchStore) {
1371
+ const result = await searchStore.dispatch('search/queryPeople', payload)
1372
+ return {
1373
+ people:
1374
+ result && Array.isArray(result.people)
1375
+ ? result.people
1376
+ : Array.isArray(this.resolvedPeople)
1377
+ ? this.resolvedPeople
1378
+ : [],
1379
+ groups: result && Array.isArray(result.groups) ? result.groups : [],
1380
+ }
1381
+ }
1382
+
1383
+ const { people } = await queryPeople({
1384
+ ...payload,
1385
+ runtimeConfig: this.resolveRuntimeConfig(),
1386
+ })
1387
+ return {
1388
+ people: Array.isArray(people) ? people : [],
1389
+ groups: [],
1390
+ }
1391
+ },
1392
+
1393
+ getContactGroupKey(group = {}) {
1394
+ return String(group.id || group.title || group.contactEmail || '').trim()
1395
+ },
1396
+
1397
+ mergeContactGroups(primaryGroups = [], relatedGroups = []) {
1398
+ const merged = []
1399
+ const keyedIndexes = new Map()
1400
+ const addGroup = group => {
1401
+ if (!group || typeof group !== 'object') return
1402
+ const key = this.getContactGroupKey(group)
1403
+ if (!key) return
1404
+
1405
+ if (keyedIndexes.has(key)) {
1406
+ const existingIndex = keyedIndexes.get(key)
1407
+ const existing = merged[existingIndex]
1408
+ if (
1409
+ existing &&
1410
+ existing.relatedPersonMatch &&
1411
+ !group.relatedPersonMatch
1412
+ ) {
1413
+ merged[existingIndex] = group
1414
+ }
1415
+ return
1416
+ }
1417
+
1418
+ keyedIndexes.set(key, merged.length)
1419
+ merged.push(group)
1420
+ }
1421
+
1422
+ primaryGroups.forEach(addGroup)
1423
+ relatedGroups.forEach(addGroup)
1424
+
1425
+ return merged
1426
+ },
1427
+
1428
+ getDirectContactGroups() {
1429
+ return (this.currentGroups || []).filter(
1430
+ group => !(group && group.relatedPersonMatch)
1431
+ )
1432
+ },
1433
+
1311
1434
  isLocalDevelopmentHost(hostname = '') {
1312
1435
  if (!hostname || typeof hostname !== 'string') return false
1313
1436
  const lower = hostname.toLowerCase()
@@ -1526,6 +1649,7 @@ export default {
1526
1649
  pages: Array.isArray(this.currentPages) ? this.currentPages : [],
1527
1650
  files: Array.isArray(this.currentFiles) ? this.currentFiles : [],
1528
1651
  groups: Array.isArray(this.currentGroups) ? this.currentGroups : [],
1652
+ people: Array.isArray(this.currentPeople) ? this.currentPeople : [],
1529
1653
  aiAnswer: this.aiAnswer || null,
1530
1654
  aiSources: Array.isArray(this.aiSources) ? this.aiSources : [],
1531
1655
  aiErrorMessage: this.aiErrorMessage || '',
@@ -1636,7 +1760,8 @@ export default {
1636
1760
  const pages = Array.isArray(snapshot.pages) ? snapshot.pages : []
1637
1761
  const files = Array.isArray(snapshot.files) ? snapshot.files : []
1638
1762
  const groups = Array.isArray(snapshot.groups) ? snapshot.groups : []
1639
- this.setSearchResults({ pages, files, groups })
1763
+ const people = Array.isArray(snapshot.people) ? snapshot.people : []
1764
+ this.setSearchResults({ pages, files, groups, people })
1640
1765
 
1641
1766
  if (!skipTabUpdate) {
1642
1767
  const preferredTab = SEARCH_ROUTE_TABS.has(targetState.tab)
@@ -2177,6 +2302,39 @@ export default {
2177
2302
  this.$emit('update:active', false)
2178
2303
  },
2179
2304
 
2305
+ onContactClick(item) {
2306
+ if (!item) return
2307
+ if (item.contactType === 'person') {
2308
+ this.selectedPerson = this.normalizePersonForModal(item.person || item)
2309
+ return
2310
+ }
2311
+ if (item.group) {
2312
+ this.selectedGroup = item.group
2313
+ }
2314
+ },
2315
+
2316
+ normalizePersonForModal(person = {}) {
2317
+ const jobTitles = Array.isArray(person.jobTitles)
2318
+ ? person.jobTitles
2319
+ : person.jobTitles
2320
+ ? [person.jobTitles]
2321
+ : person.jobTypes
2322
+ ? Array.isArray(person.jobTypes)
2323
+ ? person.jobTypes
2324
+ : [person.jobTypes]
2325
+ : []
2326
+
2327
+ return {
2328
+ ...person,
2329
+ id: person.id || person.employeeId || person.profileId || person.userId,
2330
+ displayName: person.displayName || person.name || '',
2331
+ jobTitles,
2332
+ workPhone: person.workPhone || person.phone || '',
2333
+ thumbnailUrl:
2334
+ person.thumbnailUrl || person.thumbnail || person.photoUrl || '',
2335
+ }
2336
+ },
2337
+
2180
2338
  resolveRuntimeConfig() {
2181
2339
  const runtimeConfig =
2182
2340
  this.$config ||
@@ -2341,9 +2499,11 @@ export default {
2341
2499
  this.beginSearchRun()
2342
2500
  }
2343
2501
  this.selectedGroup = null
2502
+ this.selectedPerson = null
2344
2503
  this.searchingAi = preserveAiLoading ? this.searchingAi : false
2345
2504
  this.searchingPages = false
2346
2505
  this.searchingFiles = false
2506
+ this.searchingPeople = false
2347
2507
  this.activeAiQuestionKey = preserveAiLoading
2348
2508
  ? this.activeAiQuestionKey
2349
2509
  : ''
@@ -2366,7 +2526,7 @@ export default {
2366
2526
  this.aiTranscript = []
2367
2527
  this.anchoredUserTurnId = null
2368
2528
  }
2369
- this.setSearchResults({ pages: [], files: [], groups: [] })
2529
+ this.setSearchResults({ pages: [], files: [], groups: [], people: [] })
2370
2530
  },
2371
2531
 
2372
2532
  async clearInput() {
@@ -2471,8 +2631,11 @@ export default {
2471
2631
  async searchFilesWithGroupOwners(options = {}) {
2472
2632
  const runId = options.runId || this.searchRunId
2473
2633
  const groupsPromise = this.searchGroups({ runId })
2634
+ const peoplePromise = this.searchPeople({ runId })
2474
2635
  await this.searchFiles({ runId })
2475
2636
  if (!this.isCurrentSearchRun(runId)) return []
2637
+ await peoplePromise
2638
+ if (!this.isCurrentSearchRun(runId)) return []
2476
2639
  const ownerFilters = await groupsPromise
2477
2640
  if (!this.isCurrentSearchRun(runId)) return ownerFilters
2478
2641
  if (ownerFilters.length > 0) {
@@ -2481,8 +2644,27 @@ export default {
2481
2644
  return ownerFilters
2482
2645
  },
2483
2646
 
2647
+ selectBestKeywordTabAfterContactResults({ runId, text } = {}) {
2648
+ if (!this.isCurrentSearchRun(runId)) return
2649
+ if (this.searchMode !== 'keyword') return
2650
+ if (this.currentTab !== 'Pages') return
2651
+ if (this.currentPages.length > 0 || this.contactResultCount === 0)
2652
+ return
2653
+
2654
+ this.currentTab = 'Contacts'
2655
+ this.syncSearchState({
2656
+ text: text || this.searchQuestion || this.inputText,
2657
+ mode: 'keyword',
2658
+ tab: 'Contacts',
2659
+ })
2660
+ this.$nextTick(() => this.refreshResultsScrollLayer())
2661
+ },
2662
+
2484
2663
  async searchGroups(options = {}) {
2485
2664
  const runId = options.runId || this.searchRunId
2665
+ const existingRelatedGroups = (this.currentGroups || []).filter(
2666
+ group => group && group.relatedPersonMatch
2667
+ )
2486
2668
  const searchInput = {
2487
2669
  keywords: this.keywords,
2488
2670
  question: this.searchQuestion || this.inputText,
@@ -2497,22 +2679,70 @@ export default {
2497
2679
  if (!this.isCurrentSearchRun(runId)) {
2498
2680
  return Array.from(
2499
2681
  new Set(
2500
- this.currentGroups
2682
+ this.getDirectContactGroups()
2501
2683
  .map(group => (group && group.title ? group.title.trim() : ''))
2502
2684
  .filter(Boolean)
2503
2685
  )
2504
2686
  )
2505
2687
  }
2688
+ if (existingRelatedGroups.length > 0) {
2689
+ this.setSearchResults({
2690
+ groups: this.mergeContactGroups(
2691
+ this.currentGroups,
2692
+ existingRelatedGroups
2693
+ ),
2694
+ })
2695
+ }
2506
2696
  this.persistSearchCacheSnapshot()
2507
2697
  return Array.from(
2508
2698
  new Set(
2509
- (this.currentGroups || [])
2699
+ this.getDirectContactGroups()
2510
2700
  .map(group => (group && group.title ? group.title.trim() : ''))
2511
2701
  .filter(Boolean)
2512
2702
  )
2513
2703
  )
2514
2704
  },
2515
2705
 
2706
+ async searchPeople(options = {}) {
2707
+ const runId = options.runId || this.searchRunId
2708
+ const searchInput = {
2709
+ keywords: this.keywords,
2710
+ question: this.searchQuestion || this.inputText,
2711
+ }
2712
+
2713
+ this.searchingPeople = true
2714
+ try {
2715
+ let people = []
2716
+ let relatedGroups = []
2717
+ try {
2718
+ const result = await this.queryPeopleWithFallback(searchInput)
2719
+ people = Array.isArray(result)
2720
+ ? result
2721
+ : result && Array.isArray(result.people)
2722
+ ? result.people
2723
+ : []
2724
+ relatedGroups =
2725
+ result && Array.isArray(result.groups) ? result.groups : []
2726
+ } catch (error) {
2727
+ people = []
2728
+ relatedGroups = []
2729
+ }
2730
+ if (!this.isCurrentSearchRun(runId)) return this.currentPeople
2731
+ this.setSearchResults({ people })
2732
+ if (relatedGroups.length > 0) {
2733
+ this.setSearchResults({
2734
+ groups: this.mergeContactGroups(this.currentGroups, relatedGroups),
2735
+ })
2736
+ }
2737
+ this.persistSearchCacheSnapshot()
2738
+ return this.currentPeople
2739
+ } finally {
2740
+ if (this.isCurrentSearchRun(runId)) {
2741
+ this.searchingPeople = false
2742
+ }
2743
+ }
2744
+ },
2745
+
2516
2746
  async onSubmitInput() {
2517
2747
  if (this.searchingAi) return
2518
2748
  const text = (this.inputText || '').trim()
@@ -2547,6 +2777,10 @@ export default {
2547
2777
  this.keywords = this.limitWords(text, 4)
2548
2778
  await this.searchPages({ keepAnswersTab: false, runId })
2549
2779
  this.searchFilesWithGroupOwners({ runId })
2780
+ .then(() =>
2781
+ this.selectBestKeywordTabAfterContactResults({ runId, text })
2782
+ )
2783
+ .catch(() => {})
2550
2784
  if (!this.isCurrentSearchRun(runId)) return
2551
2785
  this.currentTab = 'Pages'
2552
2786
  this.syncSearchState({ text, mode, tab: 'Pages' })
@@ -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) {