@salesforcedevs/dx-components 1.31.0 → 1.32.0-alpha.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.
@@ -13,6 +13,7 @@ dx-empty-state {
13
13
  display: none;
14
14
  }
15
15
 
16
+ /* Scrollable list: when content overflows, last visible item is cut by container to indicate more items (per spec). */
16
17
  .sidebar-content {
17
18
  overflow-y: auto;
18
19
  padding: var(--dx-g-spacing-sm) 0 var(--dx-g-spacing-2xl);
@@ -48,10 +48,6 @@
48
48
  <dx-sidebar-search
49
49
  onchange={onSearchChange}
50
50
  onloading={onSearchLoading}
51
- coveo-organization-id={coveoOrganizationId}
52
- coveo-public-access-token={coveoPublicAccessToken}
53
- coveo-search-hub={coveoSearchHub}
54
- coveo-advanced-query-config={coveoAdvancedQueryConfig}
55
51
  ></dx-sidebar-search>
56
52
  </div>
57
53
  <slot name="version-picker"></slot>
@@ -69,7 +65,7 @@
69
65
  <img
70
66
  lwc:if={isSearchLoading}
71
67
  class="loading-skeleton padding-horizontal"
72
- src="https://developer.salesforce.com/ns-assets/sidebar-loading.svg"
68
+ src="https://a.sfdcstatic.com/developer-website/prod/images/sidebar-loading.svg"
73
69
  alt="loading"
74
70
  />
75
71
  <template
@@ -1,18 +1,18 @@
1
1
  import cx from "classnames";
2
2
  import { api, track } from "lwc";
3
3
  import { TreeNode, SidebarSearchResult } from "typings/custom";
4
- import { getSidebarSearchParams } from "dxUtils/coveo";
5
4
  import { toJson } from "dxUtils/normalizers";
6
5
  import SidebarSearch from "dx/sidebarSearch";
7
6
  import { SidebarBase } from "dxBaseElements/sidebarBase";
8
7
 
9
8
  const MOBILE_SIZE_MATCH = "768px";
10
9
 
10
+ const getSearchQueryParam = (): string | null =>
11
+ typeof window !== "undefined"
12
+ ? new URLSearchParams(window.location.search).get("q")
13
+ : null;
14
+
11
15
  export default class Sidebar extends SidebarBase {
12
- @api coveoOrganizationId!: string;
13
- @api coveoPublicAccessToken!: string;
14
- @api coveoSearchHub!: string;
15
- @api coveoAdvancedQueryConfig!: string;
16
16
  @api header: string = "";
17
17
 
18
18
  @api
@@ -130,7 +130,7 @@ export default class Sidebar extends SidebarBase {
130
130
  constructor() {
131
131
  super();
132
132
 
133
- if (getSidebarSearchParams()) {
133
+ if (getSearchQueryParam()) {
134
134
  this.isSearchLoading = true;
135
135
  this.scrollToSelectedSearchResult = true;
136
136
  }
@@ -175,7 +175,7 @@ export default class Sidebar extends SidebarBase {
175
175
  this.isSearchLoading = e.detail;
176
176
  }
177
177
 
178
- private onSidebarSearchContentScroll(scrollEvent) {
178
+ private onSidebarSearchContentScroll(scrollEvent: Event) {
179
179
  if (this.isSearchLoading) {
180
180
  return;
181
181
  }
@@ -1,75 +1,70 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import debounce from "debounce";
3
3
  import {
4
- Result,
5
- ResultList,
6
- Unsubscribe,
7
- ResultsPerPage,
8
- SearchBox,
9
- buildInteractiveResult,
10
- buildResultList,
11
- buildResultsPerPage,
12
- buildSearchBox,
13
- loadAdvancedSearchQueryActions,
14
- SearchEngine,
15
- SearchAppState,
16
- loadQueryActions
17
- } from "@coveo/headless";
18
- import { buildSearchEngine, getSidebarSearchParams } from "dxUtils/coveo";
4
+ type Data360SearchCacheItem,
5
+ type Data360SearchResultItem,
6
+ fetchSearch,
7
+ getBaseUrlPath,
8
+ getStoredSearch,
9
+ isCacheStale,
10
+ setStoredSearch
11
+ } from "dxUtils/data360Search";
12
+ import { createSearchRegExp } from "dxUtils/regexps";
19
13
  import { RecentSearches } from "dxUtils/recentSearches";
20
- import { toJson } from "dxUtils/normalizers";
21
14
  import {
15
+ type HighlightedSections,
22
16
  Option,
23
17
  PopoverRequestCloseType,
24
18
  SidebarSearchResult
25
19
  } from "typings/custom";
26
20
 
27
- const RESULT_CLICK_LOCAL_STORAGE_KEY = "dx-sidebar-search-result-click";
28
- export const SESSION_KEY = "dx-sidebar-search-state";
29
-
30
- const DEFAULT_RESULT_COUNT = 20;
31
21
  const SEARCH_DEBOUNCE_DELAY = 1200;
32
22
 
33
- const UserRecentSearches = new RecentSearches();
23
+ /**
24
+ * Find all matches of the search query in text and return ranges for highlighting.
25
+ * Uses the same regex as the full-doc highlighter (createSearchRegExp) so sidebar
26
+ * and doc highlight the same phrases (including spaces between words).
27
+ */
28
+ function getHighlightRanges(
29
+ text: string,
30
+ searchQuery: string
31
+ ): HighlightedSections {
32
+ if (!text || !searchQuery.trim()) {
33
+ return [];
34
+ }
35
+ const re = createSearchRegExp(searchQuery.trim());
36
+ const ranges: HighlightedSections = [];
37
+ let match: RegExpExecArray | null;
38
+ while ((match = re.exec(text)) !== null) {
39
+ ranges.push({ offset: match.index, length: match[0].length });
40
+ }
41
+ return ranges;
42
+ }
34
43
 
35
- const normalizeCoveoAdvancedQueryValue = (version: string): string =>
36
- version?.split(".").join("\\.");
44
+ const UserRecentSearches = new RecentSearches();
37
45
 
38
- export default class SidebarSearch extends LightningElement {
39
- @api coveoOrganizationId!: string;
40
- @api coveoPublicAccessToken!: string;
41
- @api coveoSearchHub!: string;
42
- @api
43
- get coveoAdvancedQueryConfig(): { [key: string]: any } {
44
- return this._coveoAdvancedQueryConfig;
45
- }
46
- set coveoAdvancedQueryConfig(value) {
47
- const jsonValue = toJson(value);
48
- const hasChanged =
49
- this._coveoAdvancedQueryConfig &&
50
- Object.entries(jsonValue).some(
51
- ([key, val]: [string, any]) =>
52
- this._coveoAdvancedQueryConfig[key] !== val
53
- );
54
- this._coveoAdvancedQueryConfig = jsonValue;
46
+ function resultPathFromHref(href: string): string {
47
+ return href.startsWith("http") ? new URL(href).pathname : href;
48
+ }
55
49
 
56
- if (hasChanged && this.engine && this.value !== "") {
57
- this.dispatchOnLoading(true);
58
- }
50
+ function applySelectedAndSelect(
51
+ items: Data360SearchCacheItem[]
52
+ ): SidebarSearchResult[] {
53
+ const currentPath = window.location.pathname;
54
+ return items.map((item) => ({
55
+ ...item,
56
+ selected: !!item.href && resultPathFromHref(item.href) === currentPath,
57
+ select: () => {}
58
+ }));
59
+ }
59
60
 
60
- if (hasChanged && this.engine) {
61
- // set advanced filters needs access to the latest coveoAdvancedQueryConfig
62
- this.setAdvancedFilters(this.engine);
63
- this.submitSearch(true);
64
- }
65
- }
61
+ const getSearchQueryParam = (): string =>
62
+ new URLSearchParams(window.location.search).get("q") ?? "";
66
63
 
64
+ export default class SidebarSearch extends LightningElement {
67
65
  @api
68
66
  public fetchMoreResults() {
69
- if (this.resultList?.state?.moreResultsAvailable) {
70
- this.fetchingMoreResults = true;
71
- this.resultList.fetchMoreResults();
72
- }
67
+ // Data 360 Search API does not expose pagination in the same way; no-op
73
68
  }
74
69
 
75
70
  @api
@@ -85,313 +80,128 @@ export default class SidebarSearch extends LightningElement {
85
80
  this.dropdownRequestedOpen = value;
86
81
  }
87
82
 
88
- private _coveoAdvancedQueryConfig!: { [key: string]: any };
89
83
  private dropdownRequestedOpen: boolean = false;
90
84
  private recentSearches: Option[] = [];
91
- private unsubscribeFromResultList: Unsubscribe = () => {};
92
- private resultList: ResultList | null = null;
93
- private resultsPerPage: ResultsPerPage | null = null;
94
- private preloadedState?: SearchAppState;
95
- private searchBox: SearchBox | null = null;
96
- private value?: string = "";
97
- private engine: SearchEngine | null = null;
98
- private fetchingMoreResults: boolean = false;
85
+ private value: string = "";
99
86
  private didRender = false;
100
-
101
- private get hasCorrectCoveoConfig(): boolean {
102
- const coveoConfigurations = [
103
- this.coveoOrganizationId,
104
- this.coveoPublicAccessToken,
105
- this.coveoSearchHub,
106
- this.coveoAdvancedQueryConfig
107
- ];
108
-
109
- return coveoConfigurations.every(
110
- (val) =>
111
- !!val && (typeof val === "string" || typeof val === "object")
112
- );
113
- }
87
+ private data360SearchInitialized: boolean = false;
114
88
 
115
89
  private get isDropdownOpen() {
116
90
  return (
117
91
  this.dropdownRequestedOpen &&
118
- this.recentSearches &&
119
- this.recentSearches.length > 0 &&
92
+ this.recentSearches?.length > 0 &&
120
93
  !this.value
121
94
  );
122
95
  }
123
96
 
124
97
  constructor() {
125
98
  super();
126
-
127
99
  this.updateRecentSearches();
128
- this.value = getSidebarSearchParams() || "";
129
- }
130
-
131
- disconnectedCallback() {
132
- this.unsubscribeFromResultList();
100
+ this.value = getSearchQueryParam();
133
101
  }
134
102
 
135
103
  renderedCallback() {
136
- // in prod some of the critical coveo configuration attributes seem to arrive later
137
- // so I needed to put this coveo startup flow here where we can wait for them to arrive
138
-
139
- if (this.hasCorrectCoveoConfig && !this.engine) {
140
- this.initializeUserSession();
141
- this.initializeCoveo();
142
- } else if (!this.hasCorrectCoveoConfig) {
143
- console.error(
144
- "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
145
- {
146
- coveoOrganizationId: this.coveoOrganizationId,
147
- coveoPublicAccessToken: this.coveoPublicAccessToken,
148
- coveoSearchHub: this.coveoSearchHub,
149
- coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
150
- }
151
- );
152
- }
153
-
154
- // If this is the first render and there is a search value, trigger
155
- // term highlighting
156
- if (!this.didRender && this.value) {
157
- this.dispatchHighlightedTermChange();
158
- }
159
-
160
- this.didRender = true;
161
- }
162
-
163
- private initializeUserSession() {
164
- const stringified = window.sessionStorage.getItem(SESSION_KEY);
165
-
166
- if (!stringified) {
167
- return;
168
- }
169
-
170
- try {
171
- const json = JSON.parse(stringified);
172
-
173
- const hasSameConfig = Object.entries(
174
- json.coveoAdvancedQueryConfig
175
- ).every(
176
- ([key, value]: [string, any]) =>
177
- this.coveoAdvancedQueryConfig[key] === value
178
- );
179
- const hasSameQuery = json?.state?.query?.q === this.value;
180
-
181
- if (hasSameConfig && hasSameQuery) {
182
- this.preloadedState = json?.state;
183
- } else {
184
- window.sessionStorage.removeItem(SESSION_KEY);
185
- }
186
- } catch (e) {
187
- console.error(e);
188
- window.sessionStorage.removeItem(SESSION_KEY);
189
- }
190
- }
191
-
192
- private initializeCoveo = () => {
193
- try {
194
- this.engine = buildSearchEngine({
195
- organizationId: this.coveoOrganizationId,
196
- publicAccessToken: this.coveoPublicAccessToken,
197
- searchHub: this.coveoSearchHub,
198
- preloadedState: this.preloadedState
199
- });
200
- } catch (ex) {
201
- console.error(`Coveo engine failed to initialize (${ex})`);
202
- }
203
-
204
- if (!this.engine) {
205
- console.error("Coveo engine failed to initialize!");
206
- return;
207
- }
208
-
209
- if (this.preloadedState) {
210
- const actions = loadQueryActions(this.engine);
211
- this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
212
- }
213
-
214
- this.resultList = buildResultList(this.engine, {
215
- options: { fieldsToInclude: ["sfhtml_url__c"] }
216
- });
217
-
218
- this.resultsPerPage = buildResultsPerPage(this.engine, {
219
- initialState: {
220
- numberOfResults: DEFAULT_RESULT_COUNT
221
- }
222
- });
223
-
224
- this.searchBox = buildSearchBox(this.engine, {
225
- options: { numberOfSuggestions: 3 }
226
- });
227
-
228
- this.setAdvancedFilters(this.engine);
229
-
230
- this.unsubscribeFromResultList = this.resultList?.subscribe(
231
- this.onResultListChange
232
- );
233
-
234
- if (this.value) {
235
- this.dispatchHighlightedTermChange();
236
- }
237
-
238
- if (!this.preloadedState) {
104
+ if (!this.data360SearchInitialized) {
105
+ this.data360SearchInitialized = true;
239
106
  if (this.value) {
240
- this.submitSearch();
107
+ const baseUrlPath = getBaseUrlPath();
108
+ const cached = getStoredSearch(baseUrlPath);
109
+ const queryTrimmed = this.value.trim();
110
+ if (
111
+ cached?.query === queryTrimmed &&
112
+ Array.isArray(cached.results) &&
113
+ !isCacheStale(cached)
114
+ ) {
115
+ if (!this.didRender) {
116
+ this.dispatchHighlightedTermChange();
117
+ }
118
+ const results = applySelectedAndSelect(cached.results);
119
+ this.dispatchChange(results);
120
+ this.dispatchOnLoading(false);
121
+ } else {
122
+ if (!this.didRender) {
123
+ this.dispatchHighlightedTermChange();
124
+ }
125
+ this.dispatchOnLoading(true);
126
+ this.fetchDataCloudSearch();
127
+ }
241
128
  } else {
129
+ // No URL query: restore search bar and results from localStorage for this path
130
+ const baseUrlPath = getBaseUrlPath();
131
+ const cached = getStoredSearch(baseUrlPath);
132
+ if (cached?.query && !isCacheStale(cached)) {
133
+ this.value = cached.query;
134
+ const results = applySelectedAndSelect(
135
+ cached.results ?? []
136
+ );
137
+ this.dispatchChange(results);
138
+ this.dispatchHighlightedTermChange();
139
+ }
242
140
  this.dispatchOnLoading(false);
243
141
  }
244
142
  }
245
-
246
- this.syncAnalytics();
247
- };
248
-
249
- private syncAnalytics = () => {
250
- const storedResult = window.localStorage.getItem(
251
- RESULT_CLICK_LOCAL_STORAGE_KEY
252
- );
253
- if (storedResult) {
254
- const interactiveResult = buildInteractiveResult(this.engine!, {
255
- options: { result: JSON.parse(storedResult) }
256
- });
257
- interactiveResult.cancelPendingSelect();
258
- interactiveResult.select();
259
- window.localStorage.removeItem(RESULT_CLICK_LOCAL_STORAGE_KEY);
260
- }
261
- };
262
-
263
- private onResultListChange = () => {
264
- if (!this.resultList) {
265
- return;
266
- }
267
-
268
- const { isLoading, firstSearchExecuted, results } =
269
- this.resultList.state;
270
-
271
- if ((!firstSearchExecuted && !isLoading) || !this.value) {
272
- // coveo search is firing off some unwanted payloads on startup
273
- return;
274
- }
275
-
276
- if (!isLoading) {
277
- this.dispatchOnChangeEvent(results);
278
- }
279
-
280
- if (!this.fetchingMoreResults) {
281
- this.dispatchOnLoading(isLoading);
282
- }
283
-
284
- if (!isLoading && this.fetchingMoreResults) {
285
- this.fetchingMoreResults = false;
143
+ if (!this.didRender && this.value) {
144
+ this.dispatchHighlightedTermChange();
286
145
  }
287
- };
288
-
289
- private setAdvancedFilters(engine: SearchEngine) {
290
- const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
291
- (result, [key, value]) =>
292
- `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
293
- value
294
- )}")`,
295
- ""
296
- );
297
-
298
- const registerSearchQueryAction = loadAdvancedSearchQueryActions(
299
- engine
300
- ).registerAdvancedSearchQueries({
301
- aq: aq
302
- });
303
-
304
- engine.dispatch(registerSearchQueryAction);
146
+ this.didRender = true;
305
147
  }
306
148
 
307
- private normalizeCoveoResult = (result: Result): SidebarSearchResult => {
308
- const {
309
- clickUri,
310
- excerpt,
311
- excerptHighlights,
312
- raw: { sfhtml_url__c },
313
- title,
314
- titleHighlights,
315
- uniqueId
316
- } = result;
317
-
318
- // discussion about this normalization here: https://salesforce-internal.slack.com/archives/C020S6784JX/p1639506238376600
319
-
320
- let pathname, queryParam;
321
- if (sfhtml_url__c) {
322
- pathname = `/docs/${sfhtml_url__c}`;
323
- } else {
324
- const isNestedGuide = clickUri.includes("/guide/");
325
- const url = new URL(clickUri);
326
- const extension =
327
- isNestedGuide && !url.pathname?.endsWith(".html")
328
- ? ".html"
329
- : "";
330
- pathname = `${url.pathname}${extension}`;
331
- queryParam = url.search;
332
- }
333
-
334
- let href;
335
- let isSelected = false;
336
- const isReferenceUrl = clickUri.includes("/references/");
337
- if (isReferenceUrl) {
338
- if (queryParam) {
339
- href = `${pathname}${queryParam}&q=${this.value}`;
340
- } else {
341
- href = `${pathname}?q=${this.value}`;
342
- }
343
-
344
- //NOTE: This is specific to references related comparison
345
- const resultHrefWithMetaQuery = `${pathname}${queryParam}`;
346
-
347
- const urlParams = new URLSearchParams(window.location.search);
348
- const metaQueryParam = urlParams.get("meta");
349
-
350
- let currentUrlPathWithQuery = window.location.pathname;
351
- if (metaQueryParam) {
352
- currentUrlPathWithQuery = `${currentUrlPathWithQuery}?meta=${metaQueryParam}`;
353
- }
354
-
355
- isSelected = resultHrefWithMetaQuery === currentUrlPathWithQuery;
356
- } else {
357
- href = `${pathname}?q=${this.value}`;
358
- isSelected = pathname === window.location.pathname;
359
- }
360
-
149
+ private normalizeDataCloudResult = (
150
+ item: Data360SearchResultItem,
151
+ index: number
152
+ ): SidebarSearchResult => {
153
+ const href = item.url ?? "";
154
+ const resultPath = href.startsWith("http")
155
+ ? new URL(href).pathname
156
+ : href;
157
+ const isSelected =
158
+ !!resultPath && resultPath === window.location.pathname;
159
+ const title = item.title ?? "";
160
+ const excerpt = item.matchedText ?? "";
161
+ const searchQuery = this.value.trim();
361
162
  return {
362
163
  title,
363
- titleHighlights,
164
+ titleHighlights: getHighlightRanges(title, searchQuery),
364
165
  excerpt,
365
- excerptHighlights,
366
- uniqueId,
166
+ excerptHighlights: getHighlightRanges(excerpt, searchQuery),
167
+ uniqueId: href || `result-${index}`,
367
168
  href,
368
169
  selected: isSelected,
369
- select: () =>
370
- window.localStorage.setItem(
371
- RESULT_CLICK_LOCAL_STORAGE_KEY,
372
- JSON.stringify(result)
373
- )
170
+ select: () => {}
374
171
  };
375
172
  };
376
173
 
377
- private updateSessionStorage() {
378
- window.sessionStorage.setItem(
379
- SESSION_KEY,
380
- JSON.stringify({
381
- coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
382
- state: this.engine?.state
383
- })
384
- );
174
+ private async fetchDataCloudSearch(): Promise<void> {
175
+ try {
176
+ const rawResults = await fetchSearch(this.value.trim());
177
+ const results: SidebarSearchResult[] = rawResults.map(
178
+ this.normalizeDataCloudResult
179
+ );
180
+ this.dispatchChange(results);
181
+ const cacheItems: Data360SearchCacheItem[] = results.map((r) => ({
182
+ title: r.title,
183
+ titleHighlights: r.titleHighlights,
184
+ excerpt: r.excerpt,
185
+ excerptHighlights: r.excerptHighlights,
186
+ uniqueId: r.uniqueId,
187
+ href: r.href
188
+ }));
189
+ setStoredSearch(getBaseUrlPath(), {
190
+ query: this.value.trim(),
191
+ results: cacheItems
192
+ });
193
+ } catch (err) {
194
+ console.error("Data 360 Search request failed", err);
195
+ this.dispatchChange([]);
196
+ } finally {
197
+ this.dispatchOnLoading(false);
198
+ }
385
199
  }
386
200
 
387
- private dispatchOnChangeEvent(results: Result[]) {
388
- this.updateSessionStorage();
201
+ private dispatchChange(results: SidebarSearchResult[]) {
389
202
  this.dispatchEvent(
390
203
  new CustomEvent("change", {
391
- detail: {
392
- results: results.map(this.normalizeCoveoResult),
393
- value: this.value
394
- }
204
+ detail: { results, value: this.value }
395
205
  })
396
206
  );
397
207
  }
@@ -427,24 +237,18 @@ export default class SidebarSearch extends LightningElement {
427
237
  }
428
238
 
429
239
  private submitSearch = debounce((isSyncingSearchValue = false) => {
430
- if (this.searchBox) {
431
- UserRecentSearches.add(this.value);
432
- this.updateRecentSearches();
433
-
434
- // When `isSyncingSearchValue` is true, we are submitting in a case
435
- // where the search box is already synced correctly.
436
- if (!isSyncingSearchValue) {
437
- this.dispatchSidebarSearchChange(this.value);
438
- }
439
-
440
- this.searchBox.updateText(this.value || "");
441
- this.searchBox.submit();
240
+ UserRecentSearches.add(this.value);
241
+ this.updateRecentSearches();
242
+ if (!isSyncingSearchValue) {
243
+ this.dispatchSidebarSearchChange(this.value);
442
244
  }
245
+ this.fetchDataCloudSearch();
443
246
  }, SEARCH_DEBOUNCE_DELAY);
444
247
 
445
248
  private onClickRecentSearch(e: CustomEvent) {
446
249
  this.value = e.detail;
447
250
  this.dispatchSidebarSearchChange(this.value);
251
+ this.dispatchOnLoading(true);
448
252
  this.submitSearch(true);
449
253
  }
450
254
 
@@ -461,7 +265,6 @@ export default class SidebarSearch extends LightningElement {
461
265
 
462
266
  private onInputChange(e: CustomEvent) {
463
267
  this.value = e.detail;
464
-
465
268
  this.handleValueChange(false);
466
269
  this.dispatchHighlightedTermChange();
467
270
  }
@@ -477,17 +280,12 @@ export default class SidebarSearch extends LightningElement {
477
280
  private handleValueChange(isSyncingSearchValue = false) {
478
281
  if (this.value) {
479
282
  this.dispatchOnLoading(true);
480
- if (this.searchBox) {
481
- this.submitSearch(isSyncingSearchValue);
482
- }
283
+ this.submitSearch(isSyncingSearchValue);
483
284
  } else {
484
- // coveo's reaction to an empty value triggers a return of results
485
- // bootlegging our own ux here`
486
285
  this.dispatchOnLoading(false);
487
- this.dispatchOnChangeEvent([]);
488
- // Empty search values are not submitted for search, so we need to manually
489
- // trigger a search change on our own here
286
+ this.dispatchChange([]);
490
287
  this.dispatchSidebarSearchChange(this.value);
288
+ setStoredSearch(getBaseUrlPath(), { query: "", results: [] });
491
289
  }
492
290
  }
493
291
  }