@salesforcedevs/dx-components 1.43.0-alpha.0 → 1.44.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/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2020, Salesforce.com, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/lwc.config.json CHANGED
@@ -93,6 +93,7 @@
93
93
  "dx/sectionBanner",
94
94
  "dx/sectionSignup",
95
95
  "dx/sectionSmall",
96
+ "dx/searchRedirect",
96
97
  "dx/select",
97
98
  "dx/sidebar",
98
99
  "dx/sidebarOld",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.43.0-alpha.0",
3
+ "version": "1.44.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -43,5 +43,6 @@
43
43
  "eventsourcemock": "2.0.0",
44
44
  "luxon": "3.4.4",
45
45
  "msw": "^2.12.4"
46
- }
46
+ },
47
+ "gitHead": "0abdf26134a564b4a943ae4d8a99a1f66dbf6baa"
47
48
  }
@@ -1,3 +1,4 @@
1
+ @import "dxHelpers/reset";
1
2
  @import "dxHelpers/text";
2
3
 
3
4
  :host {
@@ -81,6 +82,7 @@
81
82
  color: var(--dx-g-blue-vibrant-50);
82
83
  font-family: var(--dx-g-font-display);
83
84
  font-size: var(--dx-g-text-sm);
85
+ cursor: pointer;
84
86
  }
85
87
 
86
88
  .dx-filter-clear:disabled {
@@ -88,10 +90,6 @@
88
90
  cursor: default;
89
91
  }
90
92
 
91
- .dx-search-range {
92
- margin: 0 0 var(--dx-g-spacing-lg);
93
- }
94
-
95
93
  .dx-search-main {
96
94
  flex: 1 1 0;
97
95
  min-width: 0;
@@ -31,9 +31,7 @@
31
31
  class="dx-filter-type"
32
32
  name="contentType"
33
33
  header="Content Type"
34
- single-select
35
34
  options={typeFilterOptions}
36
- value={selectedTypeValue}
37
35
  onchange={handleTypeFilterChange}
38
36
  ></dx-filter-menu>
39
37
  <dx-filter-menu
@@ -49,13 +47,6 @@
49
47
  </aside>
50
48
 
51
49
  <div class="dx-search-main">
52
- <p
53
- if:true={showRange}
54
- class="dx-search-range dx-text-body-2"
55
- aria-live="polite"
56
- >
57
- Showing {rangeStart} - {rangeEnd} of {total}
58
- </p>
59
50
  <!-- Loading -->
60
51
  <template if:true={isLoading}>
61
52
  <div class="dx-search-status" aria-live="polite">
@@ -221,8 +221,12 @@ const ALL = ROWS.map(row);
221
221
  * reflect the full result set (disjunctive facets), as the real API does.
222
222
  */
223
223
  export function mockSearchResponse(params: URLSearchParams) {
224
- const type = params.get("type");
225
- const results = type ? ALL.filter((r) => r.contentType === type) : ALL;
224
+ // `type` is a comma-separated list; selected types are OR'd.
225
+ const types = (params.get("type") ?? "").split(",").filter(Boolean);
226
+ const type = types.length > 0;
227
+ const results = type
228
+ ? ALL.filter((r) => types.includes(r.contentType))
229
+ : ALL;
226
230
  return {
227
231
  query: params.get("q") ?? "apex",
228
232
  locale: params.get("lang") ?? "en-us",
@@ -57,7 +57,6 @@ interface DisplayResult {
57
57
  interface FacetChip {
58
58
  value: string;
59
59
  label: string;
60
- count: number;
61
60
  active: boolean;
62
61
  cssClass: string;
63
62
  }
@@ -77,6 +76,15 @@ export default class SearchHybrid extends LightningElement {
77
76
  /** The page's server locale, used as the default search language. */
78
77
  @api defaultLanguage: string = "en-us";
79
78
 
79
+ /**
80
+ * Comma-separated content types to always offer in the Content Type filter
81
+ * (e.g. "docs,blog,page"), in display order. Counts come from the search
82
+ * response and default to 0. Types that appear in a response but are not
83
+ * listed here are appended, so nothing is hidden. When unset, the menu lists
84
+ * only the types in the response.
85
+ */
86
+ @api contentTypes?: string;
87
+
80
88
  /** Results per page. */
81
89
  @api limit?: number;
82
90
 
@@ -256,66 +264,76 @@ export default class SearchHybrid extends LightningElement {
256
264
  // ---- content-type facet chips (disjunctive) ---------------------------
257
265
 
258
266
  get facetChips(): FacetChip[] {
259
- const facets = this.vm?.contentTypeFacets ?? [];
260
- const selected = this.vm?.selectedType ?? null;
261
- return facets
262
- .filter((b) => b.value != null && b.value !== "")
263
- .map((b) => {
264
- const active = selected === b.value;
265
- return {
266
- value: b.value,
267
- label: this.contentTypeLabel(b.value) ?? b.value,
268
- count: b.count,
269
- active,
270
- cssClass: active
271
- ? "dx-facet-chip dx-facet-chip_active"
272
- : "dx-facet-chip"
273
- };
274
- });
267
+ const responseFacets = (this.vm?.contentTypeFacets ?? []).filter(
268
+ (b) => b.value != null && b.value !== ""
269
+ );
270
+ const configured = (this.contentTypes ?? "")
271
+ .split(",")
272
+ .map((t) => t.trim())
273
+ .filter(Boolean);
274
+ // Configured types first (stable order), then any extra types the
275
+ // response returned. Per-type counts are intentionally not shown: they
276
+ // change as filters change, which is confusing.
277
+ const values = [
278
+ ...new Set([...configured, ...responseFacets.map((b) => b.value)])
279
+ ];
280
+ const selected = this.selectedTypeList;
281
+ return values.map((value) => {
282
+ const active = selected.includes(value);
283
+ return {
284
+ value,
285
+ label: this.contentTypeLabel(value) ?? value,
286
+ active,
287
+ cssClass: active
288
+ ? "dx-facet-chip dx-facet-chip_active"
289
+ : "dx-facet-chip"
290
+ };
291
+ });
275
292
  }
276
293
 
277
294
  get hasFacets(): boolean {
278
295
  return this.facetChips.length > 0;
279
296
  }
280
297
 
281
- /** Options for the sidebar dx-filter-menu (single-select by content type, sent to the API as `type`). */
298
+ /** Options for the sidebar dx-filter-menu (multi-select by content type, sent to the API as a comma-separated `type`). */
282
299
  get typeFilterOptions(): Array<{
283
300
  id: string;
284
301
  name: string;
285
302
  value: string;
303
+ checked: boolean;
286
304
  }> {
287
305
  return this.facetChips.map((chip) => ({
288
306
  id: chip.value,
289
- name: `${chip.label} (${chip.count})`,
290
- value: chip.value
307
+ name: chip.label,
308
+ value: chip.value,
309
+ checked: chip.active
291
310
  }));
292
311
  }
293
312
 
294
- get selectedTypeValue(): string {
295
- return this.vm?.selectedType ?? "";
313
+ /** The view model's selectedType (null | string | string[]) as a list. */
314
+ private get selectedTypeList(): string[] {
315
+ const t = this.vm?.selectedType;
316
+ return t == null ? [] : Array.isArray(t) ? t : [t];
317
+ }
318
+
319
+ /** Adds `value` to the selection, or removes it if already selected. */
320
+ private toggleType(value: string): void {
321
+ const list = this.selectedTypeList;
322
+ const next = list.includes(value)
323
+ ? list.filter((t) => t !== value)
324
+ : [...list, value];
325
+ // A single type is passed as a string, several as an array.
326
+ this.controller?.setType(next.length > 1 ? next : next[0] ?? null);
296
327
  }
297
328
 
298
329
  get hasActiveType(): boolean {
299
- return !!this.vm?.selectedType;
330
+ return this.selectedTypeList.length > 0;
300
331
  }
301
332
 
302
333
  get noActiveFilters(): boolean {
303
334
  return !this.hasActiveType && this.localeValue === this.defaultLanguage;
304
335
  }
305
336
 
306
- get showRange(): boolean {
307
- return this.hasResults && this.total > 0;
308
- }
309
-
310
- get rangeStart(): number {
311
- return ((this.vm?.page ?? 1) - 1) * (this.vm?.limit ?? 10) + 1;
312
- }
313
-
314
- get rangeEnd(): number {
315
- const limit = this.vm?.limit ?? 10;
316
- return Math.min(this.rangeStart + limit - 1, this.total);
317
- }
318
-
319
337
  // ---- locale select options -------------------------------------
320
338
 
321
339
  get showLocaleSelector(): boolean {
@@ -369,8 +387,8 @@ export default class SearchHybrid extends LightningElement {
369
387
  ): void {
370
388
  const value = event.detail?.value?.value;
371
389
  if (value) {
372
- // Disjunctive toggle: the controller clears it if already active.
373
- this.controller?.setType(value);
390
+ // Toggle: the controller adds the type, or removes it if selected.
391
+ this.toggleType(value);
374
392
  }
375
393
  }
376
394
 
@@ -386,9 +404,9 @@ export default class SearchHybrid extends LightningElement {
386
404
  handleFacetClick(event: Event): void {
387
405
  const value = (event.currentTarget as HTMLElement)?.dataset?.value;
388
406
  if (value) {
389
- // Disjunctive toggle: the controller clears it if it is already
390
- // active.
391
- this.controller?.setType(value);
407
+ // Toggle: the controller adds the type, or removes it if it is
408
+ // already selected.
409
+ this.toggleType(value);
392
410
  }
393
411
  }
394
412
 
@@ -0,0 +1 @@
1
+ <template></template>
@@ -0,0 +1,111 @@
1
+ import { LightningElement, api } from "lwc";
2
+
3
+ const DEFAULT_LANG = "en-us";
4
+
5
+ // Language ids accepted by the developer-website /search page (`locales` in its
6
+ // content/en-us/pages/search.njk, lowercase BCP 47). The page ignores any ?lang=
7
+ // value outside this set.
8
+ const SEARCH_LANGS = new Set([
9
+ "en-us",
10
+ "ja-jp",
11
+ "de-de",
12
+ "fr-fr",
13
+ "es-es",
14
+ "es-mx",
15
+ "it-it",
16
+ "pt-br",
17
+ "nl-nl",
18
+ "da-dk",
19
+ "fi-fi",
20
+ "nb-no",
21
+ "sv-se",
22
+ "ru-ru",
23
+ "ko-kr",
24
+ "zh-cn",
25
+ "zh-tw"
26
+ ]);
27
+
28
+ // First-path-segment prefixes that imply a language, e.g. /jp/docs -> ja-jp.
29
+ const PATH_PREFIX_LANG: Record<string, string> = {
30
+ jp: "ja-jp",
31
+ jpblogs: "ja-jp"
32
+ };
33
+ const LOCALE_SEGMENT = /^[a-z]{2}-[a-z]{2}$/i;
34
+
35
+ /**
36
+ * Derives the search language id from a URL path. Docs carry the locale as
37
+ * /docs/<product>/<locale>/... or /docs/atlas.<locale>.<guide>/...; other pages
38
+ * lead with it (/ja-jp/...) or use a prefix such as /jp/. Returns en-us when no
39
+ * supported language is found.
40
+ */
41
+ export function langFromPath(pathname: string): string {
42
+ const segs = pathname.split("/").filter(Boolean);
43
+ const first = (segs[0] || "").toLowerCase();
44
+ if (first === "docs" && segs[1]) {
45
+ const atlas = segs[1].match(/^atlas\.([a-z]{2}-[a-z]{2})\./i);
46
+ const code = (
47
+ atlas ? atlas[1] : LOCALE_SEGMENT.test(segs[2] || "") ? segs[2] : ""
48
+ ).toLowerCase();
49
+ if (SEARCH_LANGS.has(code)) {
50
+ return code;
51
+ }
52
+ }
53
+ if (SEARCH_LANGS.has(first)) {
54
+ return first;
55
+ }
56
+ return PATH_PREFIX_LANG[first] || DEFAULT_LANG;
57
+ }
58
+
59
+ /** Builds the relative search URL, adding `lang` only for non-default languages. */
60
+ export function buildSearchUrl(
61
+ searchPath: string,
62
+ query: string,
63
+ pathname: string
64
+ ): string {
65
+ const params = new URLSearchParams({ q: query });
66
+ const lang = langFromPath(pathname);
67
+ if (lang !== DEFAULT_LANG) {
68
+ params.set("lang", lang);
69
+ }
70
+ return `${searchPath}?${params.toString()}`;
71
+ }
72
+
73
+ /**
74
+ * Redirects global nav searches to a search page on the current host.
75
+ *
76
+ * With `search-spa="true"` in the header settings, the nav doesn't navigate on
77
+ * submit; it dispatches `www_spasearch` with { searchTerm } and leaves navigation
78
+ * to the host page. This component handles that event, redirecting to
79
+ * `<searchPath>?q=<term>` and adding `&lang=<id>` when the page isn't English.
80
+ * Render it once next to <dx-global-header>.
81
+ *
82
+ * Mirrors arch-xsf-mfe-events; once Developer and Architect share a search, this
83
+ * can move into dx-global-header.
84
+ */
85
+ export default class SearchRedirect extends LightningElement {
86
+ @api searchPath: string = "/search";
87
+
88
+ connectedCallback(): void {
89
+ document.addEventListener("www_spasearch", this.handleSearch);
90
+ }
91
+
92
+ disconnectedCallback(): void {
93
+ document.removeEventListener("www_spasearch", this.handleSearch);
94
+ }
95
+
96
+ private handleSearch = (event: Event): void => {
97
+ const searchTerm = (
98
+ (event as CustomEvent<{ searchTerm?: string }>).detail
99
+ ?.searchTerm || ""
100
+ ).trim();
101
+ if (searchTerm) {
102
+ window.location.assign(
103
+ buildSearchUrl(
104
+ this.searchPath,
105
+ searchTerm,
106
+ window.location.pathname
107
+ )
108
+ );
109
+ }
110
+ };
111
+ }
@@ -5,11 +5,6 @@
5
5
  padding: var(--dx-g-spacing-sm) 0 var(--dx-g-spacing-2xl);
6
6
  }
7
7
 
8
- .trees-loading {
9
- margin: var(--dx-g-spacing-sm) var(--dx-g-spacing-lg) var(--dx-g-spacing-md)
10
- var(--dx-g-global-header-padding-horizontal);
11
- }
12
-
13
8
  dx-empty-state {
14
9
  --dx-c-empty-state-background-alt: var(--sds-g-gray-1);
15
10
  --dx-c-empty-state-image-width: 400px;
@@ -77,12 +77,6 @@
77
77
  onselecteditemrendered={onSelectedItemRendered}
78
78
  ></dx-tree>
79
79
  </div>
80
- <img
81
- lwc:elseif={loading}
82
- class="trees-loading"
83
- src="https://developer.salesforce.com/ns-assets/sidebar-item-loading.svg"
84
- alt="Loading"
85
- />
86
80
  <dx-empty-state
87
81
  variant="alt"
88
82
  class="padding-horizontal"
@@ -2,7 +2,7 @@ import cx from "classnames";
2
2
  import { api } from "lwc";
3
3
 
4
4
  import { TreeNode } from "typings/custom";
5
- import { normalizeBoolean, toJson } from "dxUtils/normalizers";
5
+ import { toJson } from "dxUtils/normalizers";
6
6
  import { SidebarBase } from "dxBaseElements/sidebarBase";
7
7
  import { escapeRegExp } from "dxUtils/regexps";
8
8
 
@@ -38,17 +38,6 @@ export default class Sidebar extends SidebarBase {
38
38
  this._emptyStateMessage = value;
39
39
  }
40
40
 
41
- /** Shows a loading placeholder instead of the empty state while the trees load. */
42
- @api
43
- get loading(): boolean {
44
- return this._loading;
45
- }
46
-
47
- set loading(value) {
48
- this._loading = normalizeBoolean(value);
49
- }
50
-
51
- private _loading = false;
52
41
  private _searchTimeout?: number;
53
42
  private _trees!: Array<TreeNode>;
54
43