@salesforcedevs/dx-components 1.28.6 → 1.28.7-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.28.6",
3
+ "version": "1.28.7-alpha.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -44,5 +44,5 @@
44
44
  "luxon": "3.4.4",
45
45
  "msw": "^2.12.4"
46
46
  },
47
- "gitHead": "586d42fe6b5363288a0b2a0661d740991a53aa68"
47
+ "gitHead": "fd19dde09113f8458a649b7f5e27fac4db3d2bb5"
48
48
  }
@@ -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>
@@ -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,42 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import debounce from "debounce";
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";
19
3
  import { RecentSearches } from "dxUtils/recentSearches";
20
- import { toJson } from "dxUtils/normalizers";
21
4
  import {
22
5
  Option,
23
6
  PopoverRequestCloseType,
24
7
  SidebarSearchResult
25
8
  } from "typings/custom";
26
9
 
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
10
  const SEARCH_DEBOUNCE_DELAY = 1200;
11
+ const DATA_CLOUD_SEARCH_PATH = "/data-cloud-search/search";
32
12
 
33
13
  const UserRecentSearches = new RecentSearches();
34
14
 
35
- const normalizeCoveoAdvancedQueryValue = (version: string): string =>
36
- version?.split(".").join("\\.");
15
+ const getSearchQueryParam = (): string =>
16
+ new URLSearchParams(window.location.search).get("q") ?? "";
17
+
18
+ /**
19
+ * Returns the current page base URL without the last path segment.
20
+ * e.g. https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dev_guide.htm
21
+ * -> https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode
22
+ */
23
+ const getBaseUrlPath = (): string => {
24
+ const url = window.location.origin + window.location.pathname;
25
+ const lastSlash = url.lastIndexOf("/");
26
+ return lastSlash > 0 ? url.substring(0, lastSlash) : url;
27
+ };
28
+
29
+ /** Data Cloud Search API result item (title, url, matchedText) */
30
+ interface DataCloudSearchResultItem {
31
+ title?: string;
32
+ url?: string;
33
+ matchedText?: string;
34
+ }
37
35
 
38
36
  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;
55
-
56
- if (hasChanged && this.engine && this.value !== "") {
57
- this.dispatchOnLoading(true);
58
- }
59
-
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
- }
66
-
67
37
  @api
68
38
  public fetchMoreResults() {
69
- if (this.resultList?.state?.moreResultsAvailable) {
70
- this.fetchingMoreResults = true;
71
- this.resultList.fetchMoreResults();
72
- }
39
+ // Data Cloud Search API does not expose pagination in the same way; no-op
73
40
  }
74
41
 
75
42
  @api
@@ -85,313 +52,98 @@ export default class SidebarSearch extends LightningElement {
85
52
  this.dropdownRequestedOpen = value;
86
53
  }
87
54
 
88
- private _coveoAdvancedQueryConfig!: { [key: string]: any };
89
55
  private dropdownRequestedOpen: boolean = false;
90
56
  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;
57
+ private value: string = "";
99
58
  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
- }
59
+ private dataCloudSearchInitialized: boolean = false;
114
60
 
115
61
  private get isDropdownOpen() {
116
62
  return (
117
63
  this.dropdownRequestedOpen &&
118
- this.recentSearches &&
119
- this.recentSearches.length > 0 &&
64
+ this.recentSearches?.length > 0 &&
120
65
  !this.value
121
66
  );
122
67
  }
123
68
 
124
69
  constructor() {
125
70
  super();
126
-
127
71
  this.updateRecentSearches();
128
- this.value = getSidebarSearchParams() || "";
129
- }
130
-
131
- disconnectedCallback() {
132
- this.unsubscribeFromResultList();
72
+ this.value = getSearchQueryParam();
133
73
  }
134
74
 
135
75
  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);
76
+ if (!this.dataCloudSearchInitialized) {
77
+ this.dataCloudSearchInitialized = true;
78
+ if (!this.didRender && this.value) {
79
+ this.dispatchHighlightedTermChange();
185
80
  }
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) {
239
- if (this.value) {
240
- this.submitSearch();
241
- } else {
81
+ if (!this.value) {
242
82
  this.dispatchOnLoading(false);
243
83
  }
244
84
  }
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;
85
+ if (!this.didRender && this.value) {
86
+ this.dispatchHighlightedTermChange();
286
87
  }
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);
88
+ this.didRender = true;
305
89
  }
306
90
 
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
-
91
+ private normalizeDataCloudResult = (
92
+ item: DataCloudSearchResultItem,
93
+ index: number
94
+ ): SidebarSearchResult => {
95
+ const href = item.url ?? "";
96
+ const resultPath = href.startsWith("http")
97
+ ? new URL(href).pathname
98
+ : href;
99
+ const isSelected =
100
+ !!resultPath && resultPath === window.location.pathname;
361
101
  return {
362
- title,
363
- titleHighlights,
364
- excerpt,
365
- excerptHighlights,
366
- uniqueId,
102
+ title: item.title ?? "",
103
+ titleHighlights: [],
104
+ excerpt: item.matchedText ?? "",
105
+ excerptHighlights: [],
106
+ uniqueId: href || `result-${index}`,
367
107
  href,
368
108
  selected: isSelected,
369
- select: () =>
370
- window.localStorage.setItem(
371
- RESULT_CLICK_LOCAL_STORAGE_KEY,
372
- JSON.stringify(result)
373
- )
109
+ select: () => {}
374
110
  };
375
111
  };
376
112
 
377
- private updateSessionStorage() {
378
- window.sessionStorage.setItem(
379
- SESSION_KEY,
380
- JSON.stringify({
381
- coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
382
- state: this.engine?.state
383
- })
384
- );
113
+ private async fetchDataCloudSearch(): Promise<void> {
114
+ const body = {
115
+ searchQuery: this.value.trim(),
116
+ baseUrlPath: getBaseUrlPath()
117
+ };
118
+ try {
119
+ const res = await fetch(DATA_CLOUD_SEARCH_PATH, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify(body)
123
+ });
124
+ if (!res.ok) {
125
+ throw new Error(`Search API error: ${res.status}`);
126
+ }
127
+ const data = await res.json();
128
+ const rawResults: DataCloudSearchResultItem[] = Array.isArray(data)
129
+ ? data
130
+ : data?.results ?? data?.data?.results ?? [];
131
+ const results: SidebarSearchResult[] = rawResults.map(
132
+ this.normalizeDataCloudResult
133
+ );
134
+ this.dispatchChange(results);
135
+ } catch (err) {
136
+ console.error("Data Cloud Search request failed", err);
137
+ this.dispatchChange([]);
138
+ } finally {
139
+ this.dispatchOnLoading(false);
140
+ }
385
141
  }
386
142
 
387
- private dispatchOnChangeEvent(results: Result[]) {
388
- this.updateSessionStorage();
143
+ private dispatchChange(results: SidebarSearchResult[]) {
389
144
  this.dispatchEvent(
390
145
  new CustomEvent("change", {
391
- detail: {
392
- results: results.map(this.normalizeCoveoResult),
393
- value: this.value
394
- }
146
+ detail: { results, value: this.value }
395
147
  })
396
148
  );
397
149
  }
@@ -427,19 +179,12 @@ export default class SidebarSearch extends LightningElement {
427
179
  }
428
180
 
429
181
  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();
182
+ UserRecentSearches.add(this.value);
183
+ this.updateRecentSearches();
184
+ if (!isSyncingSearchValue) {
185
+ this.dispatchSidebarSearchChange(this.value);
442
186
  }
187
+ this.fetchDataCloudSearch();
443
188
  }, SEARCH_DEBOUNCE_DELAY);
444
189
 
445
190
  private onClickRecentSearch(e: CustomEvent) {
@@ -461,7 +206,6 @@ export default class SidebarSearch extends LightningElement {
461
206
 
462
207
  private onInputChange(e: CustomEvent) {
463
208
  this.value = e.detail;
464
-
465
209
  this.handleValueChange(false);
466
210
  this.dispatchHighlightedTermChange();
467
211
  }
@@ -477,16 +221,10 @@ export default class SidebarSearch extends LightningElement {
477
221
  private handleValueChange(isSyncingSearchValue = false) {
478
222
  if (this.value) {
479
223
  this.dispatchOnLoading(true);
480
- if (this.searchBox) {
481
- this.submitSearch(isSyncingSearchValue);
482
- }
224
+ this.submitSearch(isSyncingSearchValue);
483
225
  } else {
484
- // coveo's reaction to an empty value triggers a return of results
485
- // bootlegging our own ux here`
486
226
  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
227
+ this.dispatchChange([]);
490
228
  this.dispatchSidebarSearchChange(this.value);
491
229
  }
492
230
  }
@@ -1,8 +1,8 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import cx from "classnames";
3
- import { CoveoHighlights } from "typings/custom";
3
+ import { HighlightedSections } from "typings/custom";
4
4
 
5
- const toChunks = (value: string, highlights: CoveoHighlights) => {
5
+ const toChunks = (value: string, highlights: HighlightedSections) => {
6
6
  if (!highlights || highlights.length < 1) {
7
7
  return [
8
8
  {
@@ -51,11 +51,11 @@ const toChunks = (value: string, highlights: CoveoHighlights) => {
51
51
 
52
52
  export default class SidebarSearchResult extends LightningElement {
53
53
  @api description!: string;
54
- @api descriptionHighlights!: CoveoHighlights;
54
+ @api descriptionHighlights!: HighlightedSections;
55
55
  @api href!: string;
56
56
  @api selected!: boolean;
57
57
  @api header!: string;
58
- @api titleHighlights!: CoveoHighlights;
58
+ @api titleHighlights!: HighlightedSections;
59
59
  @api select!: Function;
60
60
 
61
61
  private get titleChunks() {