@asd20/ui-next 2.2.2 → 2.3.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,12 @@
1
1
  # Changelog
2
2
 
3
+ # [2.3.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.2.2...ui-next-v2.3.0) (2026-04-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * harden search functions for events, page, messages, etc. ([5c9893b](https://github.com/academydistrict20/asd20-ui-next/commit/5c9893b1c943ce1df54c35c97fc0d14b42c72221))
9
+
3
10
  ## [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
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -86,6 +86,50 @@ import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
86
86
 
87
87
  // import Asd20PageContent from '../../../components/organisms/Asd20PageContent'
88
88
 
89
+ const MAX_SUGGESTED_PAGES = 10
90
+ const NO_PAGE_RESULTS_FALLBACKS = [
91
+ {
92
+ id: 'calendar',
93
+ slug: 'calendar',
94
+ title: 'Calendar',
95
+ url: '/calendar',
96
+ isNoResultsFallback: true,
97
+ categories: ['App'],
98
+ pageTypeId: 'application',
99
+ metaDescription: 'View district and school calendars.',
100
+ },
101
+ {
102
+ id: 'directory',
103
+ slug: 'directory',
104
+ title: 'Directory',
105
+ url: '/directory',
106
+ isNoResultsFallback: true,
107
+ categories: ['App'],
108
+ pageTypeId: 'application',
109
+ metaDescription: 'Find staff and department contacts.',
110
+ },
111
+ {
112
+ id: 'school-directory',
113
+ slug: 'schools',
114
+ title: 'Schools',
115
+ url: '/schools',
116
+ isNoResultsFallback: true,
117
+ categories: ['App'],
118
+ pageTypeId: 'application',
119
+ metaDescription: 'Browse all Academy District 20 schools.',
120
+ },
121
+ {
122
+ id: 'help-desk',
123
+ slug: 'help-desk',
124
+ title: 'Help Desk',
125
+ url: 'https://www.asd20.org/help-desk/',
126
+ isNoResultsFallback: true,
127
+ categories: ['Support'],
128
+ pageTypeId: 'detail-page',
129
+ metaDescription: 'Get support from the Academy District 20 Help Desk.',
130
+ },
131
+ ]
132
+
89
133
  export default {
90
134
  components: {
91
135
  Asd20List,
@@ -108,6 +152,83 @@ export default {
108
152
  },
109
153
 
110
154
  methods: {
155
+ cloneFallbackPages() {
156
+ return NO_PAGE_RESULTS_FALLBACKS.map(page => ({
157
+ ...page,
158
+ categories: Array.isArray(page.categories) ? [...page.categories] : [],
159
+ }))
160
+ },
161
+ normalizeSearchText(value = '') {
162
+ return String(value)
163
+ .toLowerCase()
164
+ .replace(/[^a-z0-9]+/g, ' ')
165
+ .trim()
166
+ },
167
+ getKeywordTokens(keywords = '') {
168
+ return Array.from(
169
+ new Set(
170
+ this.normalizeSearchText(keywords)
171
+ .split(' ')
172
+ .filter(token => token.length >= 3)
173
+ )
174
+ )
175
+ },
176
+ getSuggestedPageDedupKey(page = {}) {
177
+ const normalizedUrl = this.normalizeSearchText(page.url || '')
178
+
179
+ if (normalizedUrl) return `url:${normalizedUrl}`
180
+
181
+ const normalizedTitle = this.normalizeSearchText(page.title || '')
182
+ const normalizedSlug = this.normalizeSearchText(page.slug || '')
183
+
184
+ return `title:${normalizedTitle}|slug:${normalizedSlug}`
185
+ },
186
+ dedupeSuggestedPages(pages = []) {
187
+ const seen = new Set()
188
+
189
+ return pages.filter(page => {
190
+ const dedupKey = this.getSuggestedPageDedupKey(page)
191
+
192
+ if (!dedupKey || seen.has(dedupKey)) return false
193
+
194
+ seen.add(dedupKey)
195
+ return true
196
+ })
197
+ },
198
+ isRelevantSuggestedPage(page = {}, keywords = '') {
199
+ if (page.isNoResultsFallback) return true
200
+
201
+ const normalizedKeywords = this.normalizeSearchText(keywords)
202
+ const keywordTokens = this.getKeywordTokens(keywords)
203
+
204
+ if (!normalizedKeywords || keywordTokens.length === 0) return false
205
+
206
+ const haystack = this.normalizeSearchText([
207
+ page.title,
208
+ page.slug,
209
+ page.url,
210
+ page.metaDescription,
211
+ ...(Array.isArray(page.categories) ? page.categories : []),
212
+ ].join(' '))
213
+
214
+ if (!haystack) return false
215
+ if (haystack.includes(normalizedKeywords)) return true
216
+
217
+ return keywordTokens.some(token => haystack.includes(token))
218
+ },
219
+ sanitizeSuggestedPages(pages = [], keywords = '') {
220
+ const dedupedPages = this.dedupeSuggestedPages(
221
+ Array.isArray(pages) ? pages : []
222
+ )
223
+ const relevantPages = dedupedPages.filter(page =>
224
+ this.isRelevantSuggestedPage(page, keywords)
225
+ )
226
+ const resolvedPages = relevantPages.length
227
+ ? relevantPages
228
+ : this.cloneFallbackPages()
229
+
230
+ return resolvedPages.slice(0, MAX_SUGGESTED_PAGES)
231
+ },
111
232
  async findSuggestedPages() {
112
233
  if (typeof window !== 'undefined') {
113
234
  const keywords = window.location.pathname
@@ -115,17 +236,16 @@ export default {
115
236
  .trim()
116
237
  .toLowerCase()
117
238
  try {
118
- const { pages } = await queryPages({ keywords, top: 4 })
119
-
120
- if (Array.isArray(pages) && pages.length > 0) {
121
- this.suggestedPagesListItems = mapPagesToListItems(pages)
122
- } else {
123
- // console.warn('No pages found or pages is not an array')
124
- this.suggestedPagesListItems = []
125
- }
239
+ const { pages } = await queryPages({ keywords, top: MAX_SUGGESTED_PAGES })
240
+
241
+ this.suggestedPagesListItems = mapPagesToListItems(
242
+ this.sanitizeSuggestedPages(pages, keywords)
243
+ )
126
244
  } catch (error) {
127
245
  console.error('Error fetching suggested pages:', error)
128
- this.suggestedPagesListItems = []
246
+ this.suggestedPagesListItems = mapPagesToListItems(
247
+ this.cloneFallbackPages()
248
+ )
129
249
  }
130
250
  }
131
251
  },
@@ -1,3 +1,5 @@
1
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
2
+
1
3
  /**
2
4
  * Creates a Azure Search payload
3
5
  *
@@ -10,6 +12,11 @@ export default function buildFileSearchUrl({
10
12
  owners = [],
11
13
  tags = [],
12
14
  top = 5,
15
+ runtimeConfig = null,
16
+ searchEndpoint = '',
17
+ endpoint = '',
18
+ searchKey = '',
19
+ apiVersion = '',
13
20
  }) {
14
21
  let query = keywords || '*'
15
22
 
@@ -35,21 +42,42 @@ export default function buildFileSearchUrl({
35
42
  search: query,
36
43
  $filter: filters.join(' and '),
37
44
  $top: top,
38
- 'api-version':
39
- process.env.VUE_APP_AZURE_SEARCH_API_VERSION ||
40
- process.env.GRIDSOME_AZURE_SEARCH_API_VERSION,
41
- 'api-key':
42
- process.env.VUE_APP_AZURE_SEARCH_KEY ||
43
- process.env.GRIDSOME_AZURE_SEARCH_KEY,
45
+ 'api-version': resolveRuntimeConfigValue({
46
+ explicitValue: apiVersion,
47
+ runtimeConfig,
48
+ runtimeConfigKeys: ['azureSearchVersion'],
49
+ envKeys: [
50
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
51
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
52
+ ],
53
+ }),
54
+ 'api-key': resolveRuntimeConfigValue({
55
+ explicitValue: searchKey,
56
+ runtimeConfig,
57
+ runtimeConfigKeys: ['azureSearchKey'],
58
+ envKeys: ['VUE_APP_AZURE_SEARCH_KEY', 'GRIDSOME_AZURE_SEARCH_KEY'],
59
+ }),
44
60
  }
45
61
 
46
62
  const params = Object.entries(payload)
47
63
  .map(([key, val]) => `${key}=${val}`)
48
64
  .join('&')
49
65
 
50
- let url = `${process.env.VUE_APP_AZURE_SEARCH_ENDPOINT ||
51
- process.env
52
- .GRIDSOME_AZURE_SEARCH_ENDPOINT}/indexes/files-index/docs?${params}`
66
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
67
+ explicitValue: searchEndpoint || endpoint,
68
+ runtimeConfig,
69
+ runtimeConfigKeys: ['azureSearchEndpoint'],
70
+ envKeys: ['VUE_APP_AZURE_SEARCH_ENDPOINT', 'GRIDSOME_AZURE_SEARCH_ENDPOINT'],
71
+ })
72
+ if (!resolvedSearchEndpoint || !payload['api-version'] || !payload['api-key']) {
73
+ throw new Error(
74
+ 'Azure Search configuration is required for buildFileSearchUrl.'
75
+ )
76
+ }
77
+
78
+ let url = `${normalizeBaseUrl(
79
+ resolvedSearchEndpoint
80
+ )}/indexes/files-index/docs?${params}`
53
81
 
54
82
  return url
55
83
  }
@@ -1,5 +1,7 @@
1
1
  import _get from 'lodash/get'
2
2
 
3
+ import { resolveRuntimeConfigValue } from './runtimeConfig'
4
+
3
5
  function getConfigurationValueOrDefault({
4
6
  configurations,
5
7
  category,
@@ -29,7 +31,10 @@ function getConfigurationValueOrDefault({
29
31
  * @param {*} queryResult s
30
32
  * @returns An object of common prop values for page templates
31
33
  */
32
- export default function mapPageQueryResultToPageTemplateProps(queryResult) {
34
+ export default function mapPageQueryResultToPageTemplateProps(
35
+ queryResult,
36
+ options = {}
37
+ ) {
33
38
  // Ensure query results is defined with required properties
34
39
  if (
35
40
  !queryResult.page ||
@@ -189,6 +194,14 @@ export default function mapPageQueryResultToPageTemplateProps(queryResult) {
189
194
  }
190
195
  }
191
196
 
197
+ const defaultOrganizationId =
198
+ resolveRuntimeConfigValue({
199
+ explicitValue: options.organizationId,
200
+ runtimeConfig: options.runtimeConfig,
201
+ runtimeConfigKeys: ['organizationId'],
202
+ envKeys: ['GRIDSOME_ORGANIZATION_ID', 'VUE_APP_ORGANIZATION_ID'],
203
+ }) || '26eaf390-d8ab-11e9-a3a8-5de5bba4f125'
204
+
192
205
  // Output page props
193
206
  let pageProps = {}
194
207
  try {
@@ -217,11 +230,8 @@ export default function mapPageQueryResultToPageTemplateProps(queryResult) {
217
230
  .map(l => l.link)
218
231
  .filter(l => l.type === 'Link'),
219
232
  organization: pageData.ownerOrganization || {
220
- id:
221
- process.env.GRIDSOME_ORGANIZATION_ID ||
222
- process.env.VUE_APP_ORGANIZATION_ID ||
223
- '26eaf390-d8ab-11e9-a3a8-5de5bba4f125', // District Org Id
224
- title: 'Academy District 20',
233
+ id: defaultOrganizationId, // District Org Id
234
+ title: options.organizationTitle || 'Academy District 20',
225
235
  },
226
236
 
227
237
  // TODO: add groups / department / committee table to Hasura
@@ -1,10 +1,9 @@
1
1
  import arraySearch from './arraySearch'
2
2
  import axios from 'axios'
3
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
3
4
  // import departments from '../data/departments.json'
4
5
 
5
- const API_ENDPOINT =
6
- process.env.VUE_APP_API_ENDPOINT || process.env.GRIDSOME_API_ENDPOINT
7
- let cachedDepartments = []
6
+ const cachedDepartmentsByEndpoint = new Map()
8
7
 
9
8
  /**
10
9
  * Retrieve departments from API
@@ -13,15 +12,20 @@ let cachedDepartments = []
13
12
  * @param {*} { commit }
14
13
  * @returns
15
14
  */
16
- async function getDepartments() {
17
- if (cachedDepartments.length === 0) {
18
- let { data: departments } = await axios.get(
19
- `${API_ENDPOINT}/api/departments`
15
+ async function getDepartments(apiEndpoint) {
16
+ const normalizedApiEndpoint = normalizeBaseUrl(apiEndpoint)
17
+ if (!normalizedApiEndpoint) {
18
+ throw new Error('apiEndpoint is required for queryDepartments.')
19
+ }
20
+
21
+ if (!cachedDepartmentsByEndpoint.has(normalizedApiEndpoint)) {
22
+ const { data: departments } = await axios.get(
23
+ `${normalizedApiEndpoint}/api/departments`
20
24
  )
21
- console.log(departments)
22
- cachedDepartments = departments
25
+ cachedDepartmentsByEndpoint.set(normalizedApiEndpoint, departments)
23
26
  }
24
- return cachedDepartments
27
+
28
+ return cachedDepartmentsByEndpoint.get(normalizedApiEndpoint) || []
25
29
  }
26
30
 
27
31
  /**
@@ -29,10 +33,22 @@ async function getDepartments() {
29
33
  *
30
34
  * @param {*} {keywords = '', top = 0}
31
35
  */
32
- export default async function queryDepartments({ keywords = '', top = 0 }) {
36
+ export default async function queryDepartments({
37
+ keywords = '',
38
+ top = 0,
39
+ apiEndpoint = '',
40
+ runtimeConfig = null,
41
+ }) {
33
42
  if (!keywords) return []
34
43
 
35
- let departments = await getDepartments()
44
+ const resolvedApiEndpoint = resolveRuntimeConfigValue({
45
+ explicitValue: apiEndpoint,
46
+ runtimeConfig,
47
+ runtimeConfigKeys: ['apiEndpoint'],
48
+ envKeys: ['VUE_APP_API_ENDPOINT', 'GRIDSOME_API_ENDPOINT'],
49
+ })
50
+
51
+ let departments = await getDepartments(resolvedApiEndpoint)
36
52
 
37
53
  let results = departments.filter(d => {
38
54
  let options = d.searchTerms
@@ -1,5 +1,8 @@
1
1
  import axios from 'axios'
2
2
 
3
+ import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
5
+
3
6
  /**
4
7
  * Creates a Azure Search payload
5
8
  *
@@ -73,18 +76,41 @@ export default async function queryEvents({
73
76
  limit = 5,
74
77
  requireKeywords = false,
75
78
  indexName = 'events-index',
79
+ requestUrl = '',
80
+ runtimeConfig = null,
81
+ searchKey = '',
82
+ apiVersion = '',
83
+ searchEndpoint = '',
84
+ endpoint = '',
76
85
  }) {
77
- const env = {
78
- searchKey:
79
- process.env.VUE_APP_AZURE_SEARCH_KEY ||
80
- process.env.GRIDSOME_AZURE_SEARCH_KEY,
81
- apiVersion:
82
- process.env.VUE_APP_AZURE_SEARCH_API_VERSION ||
83
- process.env.GRIDSOME_AZURE_SEARCH_API_VERSION,
84
- endpoint:
85
- process.env.VUE_APP_AZURE_SEARCH_ENDPOINT ||
86
- process.env.GRIDSOME_AZURE_SEARCH_ENDPOINT,
87
- }
86
+ const resolvedSearchKey = resolveRuntimeConfigValue({
87
+ explicitValue: searchKey,
88
+ runtimeConfig,
89
+ runtimeConfigKeys: ['azureSearchKey'],
90
+ envKeys: ['VUE_APP_AZURE_SEARCH_KEY', 'GRIDSOME_AZURE_SEARCH_KEY'],
91
+ })
92
+ const resolvedApiVersion = resolveRuntimeConfigValue({
93
+ explicitValue: apiVersion,
94
+ runtimeConfig,
95
+ runtimeConfigKeys: ['azureSearchVersion'],
96
+ envKeys: [
97
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
98
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
99
+ ],
100
+ })
101
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
102
+ explicitValue: searchEndpoint || endpoint,
103
+ runtimeConfig,
104
+ runtimeConfigKeys: ['azureSearchEndpoint'],
105
+ envKeys: ['VUE_APP_AZURE_SEARCH_ENDPOINT', 'GRIDSOME_AZURE_SEARCH_ENDPOINT'],
106
+ })
107
+ const resolvedRequestUrl =
108
+ typeof requestUrl === 'string' && requestUrl.trim()
109
+ ? requestUrl.trim()
110
+ : !resolvedSearchEndpoint &&
111
+ hasSearchProxyRuntime(runtimeConfig)
112
+ ? resolveSearchProxyUrl('events', runtimeConfig)
113
+ : ''
88
114
 
89
115
  if (!keywords && requireKeywords)
90
116
  return {
@@ -92,27 +118,49 @@ export default async function queryEvents({
92
118
  facets: [],
93
119
  }
94
120
 
95
- let result = await axios({
96
- method: 'post',
97
- headers: {
98
- 'api-key': env.searchKey,
99
- },
100
- params: {
101
- 'api-version': env.apiVersion,
102
- },
103
- url: `${env.endpoint}/indexes/${indexName}/docs/search`,
104
- data: Object.assign(
105
- {},
106
- searchPayload({
107
- startDate,
108
- endDate,
109
- keywords,
110
- calendarIds,
111
- enableFuzzySearch: false,
112
- limit,
121
+ if (
122
+ !resolvedRequestUrl &&
123
+ (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
124
+ ) {
125
+ throw new Error('Azure Search or proxy configuration is required for queryEvents.')
126
+ }
127
+
128
+ const payload = {
129
+ startDate,
130
+ endDate,
131
+ keywords,
132
+ calendarIds,
133
+ limit,
134
+ indexName,
135
+ }
136
+
137
+ const result = resolvedRequestUrl
138
+ ? await axios({
139
+ method: 'post',
140
+ url: resolvedRequestUrl,
141
+ data: payload,
142
+ })
143
+ : await axios({
144
+ method: 'post',
145
+ headers: {
146
+ 'api-key': resolvedSearchKey,
147
+ },
148
+ params: {
149
+ 'api-version': resolvedApiVersion,
150
+ },
151
+ url: `${normalizeBaseUrl(resolvedSearchEndpoint)}/indexes/${indexName}/docs/search`,
152
+ data: Object.assign(
153
+ {},
154
+ searchPayload({
155
+ startDate,
156
+ endDate,
157
+ keywords,
158
+ calendarIds,
159
+ enableFuzzySearch: false,
160
+ limit,
161
+ })
162
+ ),
113
163
  })
114
- ),
115
- })
116
164
 
117
165
  const { data } = result
118
166
 
@@ -1,5 +1,8 @@
1
1
  import axios from 'axios'
2
2
 
3
+ import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
5
+
3
6
  /**
4
7
  * Creates a Azure Search payload
5
8
  *
@@ -64,8 +67,17 @@ export default async function queryFiles({
64
67
  keywords = '',
65
68
  categories = [],
66
69
  owners = [],
70
+ organizations = [],
67
71
  top = 5,
72
+ limit = 0,
68
73
  requireKeywords = false,
74
+ indexName = 'files-index',
75
+ requestUrl = '',
76
+ runtimeConfig = null,
77
+ searchKey = '',
78
+ apiVersion = '',
79
+ searchEndpoint = '',
80
+ endpoint = '',
69
81
  }) {
70
82
  if (!keywords && requireKeywords)
71
83
  return {
@@ -73,32 +85,81 @@ export default async function queryFiles({
73
85
  facets: [],
74
86
  }
75
87
 
76
- let { data } = await axios({
77
- method: 'post',
78
- headers: {
79
- 'api-key':
80
- process.env.VUE_APP_AZURE_SEARCH_KEY ||
81
- process.env.GRIDSOME_AZURE_SEARCH_KEY,
82
- },
83
- params: {
84
- 'api-version':
85
- process.env.VUE_APP_AZURE_SEARCH_API_VERSION ||
86
- process.env.GRIDSOME_AZURE_SEARCH_API_VERSION,
87
- },
88
- url: `${process.env.VUE_APP_AZURE_SEARCH_ENDPOINT ||
89
- process.env
90
- .GRIDSOME_AZURE_SEARCH_ENDPOINT}/indexes/files-index/docs/search`,
91
- data: Object.assign(
92
- {},
93
- searchPayload({
94
- keywords,
95
- categories,
96
- owners,
97
- enableFuzzySearch: true,
98
- }),
99
- { top }
100
- ),
88
+ const resolvedLimit = Number.isFinite(Number(limit)) && Number(limit) > 0
89
+ ? Number(limit)
90
+ : top
91
+ const resolvedSearchKey = resolveRuntimeConfigValue({
92
+ explicitValue: searchKey,
93
+ runtimeConfig,
94
+ runtimeConfigKeys: ['azureSearchKey'],
95
+ envKeys: ['VUE_APP_AZURE_SEARCH_KEY', 'GRIDSOME_AZURE_SEARCH_KEY'],
96
+ })
97
+ const resolvedApiVersion = resolveRuntimeConfigValue({
98
+ explicitValue: apiVersion,
99
+ runtimeConfig,
100
+ runtimeConfigKeys: ['azureSearchVersion'],
101
+ envKeys: [
102
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
103
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
104
+ ],
101
105
  })
106
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
107
+ explicitValue: searchEndpoint || endpoint,
108
+ runtimeConfig,
109
+ runtimeConfigKeys: ['azureSearchEndpoint'],
110
+ envKeys: ['VUE_APP_AZURE_SEARCH_ENDPOINT', 'GRIDSOME_AZURE_SEARCH_ENDPOINT'],
111
+ })
112
+ const resolvedRequestUrl =
113
+ typeof requestUrl === 'string' && requestUrl.trim()
114
+ ? requestUrl.trim()
115
+ : !resolvedSearchEndpoint &&
116
+ hasSearchProxyRuntime(runtimeConfig)
117
+ ? resolveSearchProxyUrl('files', runtimeConfig)
118
+ : ''
119
+
120
+ if (
121
+ !resolvedRequestUrl &&
122
+ (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
123
+ ) {
124
+ throw new Error('Azure Search or proxy configuration is required for queryFiles.')
125
+ }
126
+
127
+ const payload = {
128
+ keywords,
129
+ categories,
130
+ owners,
131
+ organizations,
132
+ limit: resolvedLimit,
133
+ indexName,
134
+ }
135
+
136
+ const { data } = resolvedRequestUrl
137
+ ? await axios({
138
+ method: 'post',
139
+ url: resolvedRequestUrl,
140
+ data: payload,
141
+ })
142
+ : await axios({
143
+ method: 'post',
144
+ headers: {
145
+ 'api-key': resolvedSearchKey,
146
+ },
147
+ params: {
148
+ 'api-version': resolvedApiVersion,
149
+ },
150
+ url: `${normalizeBaseUrl(resolvedSearchEndpoint)}/indexes/${indexName}/docs/search`,
151
+ data: Object.assign(
152
+ {},
153
+ searchPayload({
154
+ keywords,
155
+ categories,
156
+ owners,
157
+ organizations,
158
+ enableFuzzySearch: true,
159
+ }),
160
+ { top: resolvedLimit }
161
+ ),
162
+ })
102
163
 
103
164
  // Return results
104
165
  return {
@@ -1,5 +1,8 @@
1
1
  import axios from 'axios'
2
2
 
3
+ import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
5
+
3
6
  /**
4
7
  * Creates a Azure Search payload
5
8
  *
@@ -83,22 +86,58 @@ export default async function queryMessages({
83
86
  organizationIds = [],
84
87
  categories = [],
85
88
  channels = [],
89
+ destinationIds = [],
90
+ destinationTitle = [],
86
91
  tags = [],
92
+ isInLeadershipGroup = false,
87
93
  limit = 5,
88
94
  requireKeywords = false,
89
95
  indexName = 'messages-index',
96
+ requestUrl = '',
97
+ runtimeConfig = null,
98
+ searchKey = '',
99
+ apiVersion = '',
100
+ searchEndpoint = '',
101
+ endpoint = '',
90
102
  }) {
91
- const env = {
92
- searchKey:
93
- process.env.VUE_APP_AZURE_SEARCH_MESSAGES_KEY ||
94
- process.env.GRIDSOME_AZURE_MESSAGES_SEARCH_KEY,
95
- apiVersion:
96
- process.env.VUE_APP_AZURE_SEARCH_API_VERSION ||
97
- process.env.GRIDSOME_AZURE_SEARCH_API_VERSION,
98
- endpoint:
99
- process.env.VUE_APP_AZURE_SEARCH_MESSAGES_ENDPOINT ||
100
- process.env.GRIDSOME_AZURE_MESSAGES_SEARCH_ENDPOINT,
101
- }
103
+ const resolvedSearchKey = resolveRuntimeConfigValue({
104
+ explicitValue: searchKey,
105
+ runtimeConfig,
106
+ runtimeConfigKeys: ['azureSearchMessagesKey', 'azureSearchKey'],
107
+ envKeys: [
108
+ 'VUE_APP_AZURE_SEARCH_MESSAGES_KEY',
109
+ 'GRIDSOME_AZURE_MESSAGES_SEARCH_KEY',
110
+ 'VUE_APP_AZURE_SEARCH_KEY',
111
+ 'GRIDSOME_AZURE_SEARCH_KEY',
112
+ ],
113
+ })
114
+ const resolvedApiVersion = resolveRuntimeConfigValue({
115
+ explicitValue: apiVersion,
116
+ runtimeConfig,
117
+ runtimeConfigKeys: ['azureSearchVersion'],
118
+ envKeys: [
119
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
120
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
121
+ ],
122
+ })
123
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
124
+ explicitValue: searchEndpoint || endpoint,
125
+ runtimeConfig,
126
+ runtimeConfigKeys: ['azureSearchMessagesEndpoint', 'azureSearchEndpoint'],
127
+ envKeys: [
128
+ 'VUE_APP_AZURE_SEARCH_MESSAGES_ENDPOINT',
129
+ 'GRIDSOME_AZURE_MESSAGES_SEARCH_ENDPOINT',
130
+ 'VUE_APP_AZURE_SEARCH_ENDPOINT',
131
+ 'GRIDSOME_AZURE_SEARCH_ENDPOINT',
132
+ ],
133
+ })
134
+ const resolvedRequestUrl =
135
+ typeof requestUrl === 'string' && requestUrl.trim()
136
+ ? requestUrl.trim()
137
+ : !resolvedSearchEndpoint &&
138
+ hasSearchProxyRuntime(runtimeConfig)
139
+ ? resolveSearchProxyUrl('messages', runtimeConfig)
140
+ : ''
102
141
 
103
142
  if (!keywords && requireKeywords)
104
143
  return {
@@ -106,29 +145,57 @@ export default async function queryMessages({
106
145
  facets: [],
107
146
  }
108
147
 
109
- let { data } = await axios({
110
- method: 'post',
111
- headers: {
112
- 'api-key': env.searchKey,
113
- },
114
- params: {
115
- 'api-version': env.apiVersion,
116
- },
117
- url: `${env.endpoint}/indexes/${indexName}/docs/search`,
118
- data: Object.assign(
119
- {},
120
- searchPayload({
121
- keywords,
122
- organizationIds,
123
- categories,
124
- channels,
125
- tags,
126
- enableFuzzySearch: true,
127
- limit,
128
- }),
129
- { top: limit }
130
- ),
131
- })
148
+ if (
149
+ !resolvedRequestUrl &&
150
+ (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
151
+ ) {
152
+ throw new Error(
153
+ 'Azure Search or proxy configuration is required for queryMessages.'
154
+ )
155
+ }
156
+
157
+ const payload = {
158
+ keywords,
159
+ organizationIds,
160
+ destinationIds,
161
+ destinationTitle,
162
+ categories,
163
+ channels,
164
+ tags,
165
+ isInLeadershipGroup,
166
+ limit,
167
+ indexName,
168
+ }
169
+
170
+ const { data } = resolvedRequestUrl
171
+ ? await axios({
172
+ method: 'post',
173
+ url: resolvedRequestUrl,
174
+ data: payload,
175
+ })
176
+ : await axios({
177
+ method: 'post',
178
+ headers: {
179
+ 'api-key': resolvedSearchKey,
180
+ },
181
+ params: {
182
+ 'api-version': resolvedApiVersion,
183
+ },
184
+ url: `${normalizeBaseUrl(resolvedSearchEndpoint)}/indexes/${indexName}/docs/search`,
185
+ data: Object.assign(
186
+ {},
187
+ searchPayload({
188
+ keywords,
189
+ organizationIds,
190
+ categories,
191
+ channels,
192
+ tags,
193
+ enableFuzzySearch: true,
194
+ limit,
195
+ }),
196
+ { top: limit }
197
+ ),
198
+ })
132
199
 
133
200
  // Return results
134
201
  return {
@@ -1,6 +1,9 @@
1
1
  import axios from 'axios'
2
2
 
3
- const NO_PAGE_RESULTS_FALLBACKS = [
3
+ import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
+ import { resolveRuntimeConfigValue } from './runtimeConfig'
5
+
6
+ export const NO_PAGE_RESULTS_FALLBACKS = [
4
7
  {
5
8
  id: 'calendar',
6
9
  slug: 'calendar',
@@ -106,6 +109,14 @@ export default async function queryPages({
106
109
  keywords = '',
107
110
  top = 25,
108
111
  requireKeywords = false,
112
+ organizations = [],
113
+ requestUrl = '',
114
+ runtimeConfig = null,
115
+ searchEndpoint = '',
116
+ endpoint = '',
117
+ searchKey = '',
118
+ apiVersion = '',
119
+ searchVersion = '',
109
120
  }) {
110
121
  if (!keywords && requireKeywords)
111
122
  return {
@@ -113,26 +124,82 @@ export default async function queryPages({
113
124
  facets: [],
114
125
  }
115
126
 
116
- let { data } = await axios({
117
- method: 'post',
118
- headers: {
119
- 'api-key': process.env.AZURE_SEARCH_KEY,
120
- },
121
- params: {
122
- 'api-version': process.env.AZURE_SEARCH_VERSION,
123
- },
124
- url: `${process.env.AZURE_SEARCH_ENDPOINT}`,
125
- data: Object.assign(
126
- {},
127
- searchPayload({
128
- keywords,
129
- enableFuzzySearch: true,
130
- }),
131
- {
132
- top,
133
- }
134
- ),
127
+ const resolvedSearchKey = resolveRuntimeConfigValue({
128
+ explicitValue: searchKey,
129
+ runtimeConfig,
130
+ runtimeConfigKeys: ['azureSearchKey'],
131
+ envKeys: [
132
+ 'AZURE_SEARCH_KEY',
133
+ 'VUE_APP_AZURE_SEARCH_KEY',
134
+ 'GRIDSOME_AZURE_SEARCH_KEY',
135
+ ],
136
+ })
137
+ const resolvedApiVersion = resolveRuntimeConfigValue({
138
+ explicitValue: apiVersion || searchVersion,
139
+ runtimeConfig,
140
+ runtimeConfigKeys: ['azureSearchVersion'],
141
+ envKeys: [
142
+ 'AZURE_SEARCH_VERSION',
143
+ 'VUE_APP_AZURE_SEARCH_API_VERSION',
144
+ 'GRIDSOME_AZURE_SEARCH_API_VERSION',
145
+ ],
146
+ })
147
+ const resolvedSearchEndpoint = resolveRuntimeConfigValue({
148
+ explicitValue: searchEndpoint || endpoint,
149
+ runtimeConfig,
150
+ runtimeConfigKeys: ['azureSearchEndpoint'],
151
+ envKeys: [
152
+ 'AZURE_SEARCH_ENDPOINT',
153
+ 'VUE_APP_AZURE_SEARCH_ENDPOINT',
154
+ 'GRIDSOME_AZURE_SEARCH_ENDPOINT',
155
+ ],
135
156
  })
157
+ const resolvedRequestUrl =
158
+ typeof requestUrl === 'string' && requestUrl.trim()
159
+ ? requestUrl.trim()
160
+ : !resolvedSearchEndpoint &&
161
+ hasSearchProxyRuntime(runtimeConfig)
162
+ ? resolveSearchProxyUrl('pages', runtimeConfig)
163
+ : ''
164
+
165
+ if (
166
+ !resolvedRequestUrl &&
167
+ (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
168
+ ) {
169
+ throw new Error('Azure Search or proxy configuration is required for queryPages.')
170
+ }
171
+
172
+ const { data } = resolvedRequestUrl
173
+ ? await axios({
174
+ method: 'post',
175
+ url: resolvedRequestUrl,
176
+ data: {
177
+ keywords,
178
+ organizations,
179
+ top,
180
+ },
181
+ })
182
+ : await axios({
183
+ method: 'post',
184
+ headers: {
185
+ 'api-key': resolvedSearchKey,
186
+ },
187
+ params: {
188
+ 'api-version': resolvedApiVersion,
189
+ },
190
+ url: `${resolvedSearchEndpoint}`,
191
+ data: Object.assign(
192
+ {},
193
+ searchPayload({
194
+ keywords,
195
+ enableFuzzySearch: true,
196
+ organizations,
197
+ }),
198
+ {
199
+ top,
200
+ }
201
+ ),
202
+ })
136
203
 
137
204
  const pages = Array.isArray(data.value) ? data.value : []
138
205
  const resolvedPages = pages.length
@@ -0,0 +1,80 @@
1
+ function isPlainObject(value) {
2
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
3
+ }
4
+
5
+ function normalizeConfigValue(value) {
6
+ return typeof value === 'string' ? value.trim() : value
7
+ }
8
+
9
+ function hasConfigValue(value) {
10
+ if (typeof value === 'string') {
11
+ return value.trim().length > 0
12
+ }
13
+
14
+ return value !== null && value !== undefined
15
+ }
16
+
17
+ export function resolvePublicRuntimeConfig(runtimeConfig = {}) {
18
+ if (!isPlainObject(runtimeConfig)) {
19
+ return {}
20
+ }
21
+
22
+ if (isPlainObject(runtimeConfig.public)) {
23
+ return runtimeConfig.public
24
+ }
25
+
26
+ return runtimeConfig
27
+ }
28
+
29
+ export function resolveWindowPublicRuntimeConfig() {
30
+ if (typeof window === 'undefined' || !window.__NUXT__) {
31
+ return {}
32
+ }
33
+
34
+ return resolvePublicRuntimeConfig(window.__NUXT__.config)
35
+ }
36
+
37
+ export function resolveRuntimeConfigValue({
38
+ explicitValue = null,
39
+ runtimeConfig = {},
40
+ runtimeConfigKeys = [],
41
+ envKeys = [],
42
+ } = {}) {
43
+ const normalizedExplicitValue = normalizeConfigValue(explicitValue)
44
+ if (hasConfigValue(normalizedExplicitValue)) {
45
+ return normalizedExplicitValue
46
+ }
47
+
48
+ const sources = [
49
+ resolvePublicRuntimeConfig(runtimeConfig),
50
+ resolveWindowPublicRuntimeConfig(),
51
+ ]
52
+
53
+ for (const source of sources) {
54
+ for (const key of runtimeConfigKeys) {
55
+ const candidate = normalizeConfigValue(source[key])
56
+ if (hasConfigValue(candidate)) {
57
+ return candidate
58
+ }
59
+ }
60
+ }
61
+
62
+ if (typeof process !== 'undefined' && process && process.env) {
63
+ for (const key of envKeys) {
64
+ const candidate = normalizeConfigValue(process.env[key])
65
+ if (hasConfigValue(candidate)) {
66
+ return candidate
67
+ }
68
+ }
69
+ }
70
+
71
+ return null
72
+ }
73
+
74
+ export function normalizeBaseUrl(value) {
75
+ if (typeof value !== 'string') {
76
+ return ''
77
+ }
78
+
79
+ return value.trim().replace(/\/+$/, '')
80
+ }
@@ -0,0 +1,43 @@
1
+ import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
2
+
3
+ const SEARCH_PROXY_PATHS = Object.freeze({
4
+ messages: '/api/search/messages',
5
+ pages: '/api/search/pages',
6
+ files: '/api/search/files',
7
+ events: '/api/search/events',
8
+ })
9
+
10
+ export function resolveSearchProxyPath(kind) {
11
+ return SEARCH_PROXY_PATHS[kind] || ''
12
+ }
13
+
14
+ export function hasSearchProxyRuntime(runtimeConfig = {}) {
15
+ const functionsEndpoint = resolveRuntimeConfigValue({
16
+ runtimeConfig,
17
+ runtimeConfigKeys: ['functionsEndpoint'],
18
+ envKeys: [],
19
+ })
20
+
21
+ if (functionsEndpoint) {
22
+ return true
23
+ }
24
+
25
+ return typeof window !== 'undefined' && !!window.__NUXT__
26
+ }
27
+
28
+ export function resolveSearchProxyUrl(kind, runtimeConfig = {}) {
29
+ const path = resolveSearchProxyPath(kind)
30
+
31
+ if (!path) {
32
+ throw new Error(`Unknown search proxy kind: ${String(kind)}`)
33
+ }
34
+
35
+ const functionsEndpoint = resolveRuntimeConfigValue({
36
+ runtimeConfig,
37
+ runtimeConfigKeys: ['functionsEndpoint'],
38
+ envKeys: ['FUNCTIONS_ENDPOINT', 'VUE_APP_FUNCTIONS_ENDPOINT'],
39
+ })
40
+ const baseUrl = normalizeBaseUrl(functionsEndpoint)
41
+
42
+ return baseUrl ? `${baseUrl}${path}` : path
43
+ }
@@ -1,14 +1,38 @@
1
1
  import axios from 'axios'
2
2
 
3
+ import { resolveRuntimeConfigValue } from './runtimeConfig'
4
+
3
5
  function issueIsValid(issue) {
4
6
  return !!issue.firstName && !!issue.lastName && !!issue.email && !!issue.issue
5
7
  }
6
8
 
7
- export default async function sendAccessibilityIssue(issue, endpoint, apiKey, authCode) {
9
+ export default async function sendAccessibilityIssue(
10
+ issue,
11
+ endpoint,
12
+ apiKey,
13
+ authCode,
14
+ options = {}
15
+ ) {
8
16
  if (!issueIsValid(issue)) {
9
17
  throw new Error('The issue object is incomplete or invalid.')
10
18
  }
11
19
 
20
+ const resolvedOptions =
21
+ options && typeof options === 'object' && !Array.isArray(options)
22
+ ? options
23
+ : {
24
+ categoryId: options,
25
+ }
26
+ const categoryId = resolveRuntimeConfigValue({
27
+ explicitValue: resolvedOptions.categoryId,
28
+ runtimeConfig: resolvedOptions.runtimeConfig,
29
+ runtimeConfigKeys: ['happyFoxCategoryId'],
30
+ envKeys: ['HAPPYFOX_CATEGORY_ID'],
31
+ })
32
+ if (!categoryId) {
33
+ throw new Error('happyFoxCategoryId is required to send an accessibility issue.')
34
+ }
35
+
12
36
  const payload = {
13
37
  name: `${issue.firstName} ${issue.lastName}`,
14
38
  email: issue.email,
@@ -20,7 +44,7 @@ export default async function sendAccessibilityIssue(issue, endpoint, apiKey, au
20
44
  <p><strong>Mailing Address:</strong> ${issue.address || 'Not provided'}</p>
21
45
  <p><strong>Organization:</strong> ${issue.organization || 'Not provided'}</p>
22
46
  `,
23
- category: Number(process.env.HAPPYFOX_CATEGORY_ID),
47
+ category: Number(categoryId),
24
48
  }
25
49
 
26
50
  try {