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