@salesforcedevs/dx-components 0.16.1 → 0.18.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.
Files changed (62) hide show
  1. package/lwc.config.json +2 -1
  2. package/package.json +3 -3
  3. package/src/assets/svg/sidebar-loading.svg +34 -0
  4. package/src/modules/dx/banner/__tests__/banner.test.ts +31 -0
  5. package/src/modules/dx/{feedbackBanner/feedbackBanner.css → banner/banner.css} +6 -6
  6. package/src/modules/dx/banner/banner.html +13 -0
  7. package/src/modules/dx/banner/banner.ts +14 -0
  8. package/src/modules/dx/dropdown/dropdown.html +2 -0
  9. package/src/modules/dx/dropdown/dropdown.ts +4 -2
  10. package/src/modules/dx/dropdownOption/__tests__/dropdownOption.test.ts +32 -2
  11. package/src/modules/dx/dropdownOption/dropdownOption.ts +9 -6
  12. package/src/modules/dx/footer/footer.ts +13 -1
  13. package/src/modules/dx/header/__tests__/header.test.ts +38 -21
  14. package/src/modules/dx/header/__tests__/mockProps.ts +29 -37
  15. package/src/modules/dx/header/header.html +4 -1
  16. package/src/modules/dx/header/header.stories.ts +30 -0
  17. package/src/modules/dx/header/header.ts +4 -1
  18. package/src/modules/dx/headerNav/headerNav.html +1 -0
  19. package/src/modules/dx/headerSearch/__tests__/headerSearch.test.ts +24 -39
  20. package/src/modules/dx/headerSearch/__tests__/mockProps.ts +2 -7
  21. package/src/modules/dx/headerSearch/headerSearch.ts +13 -19
  22. package/src/modules/dx/lazy/lazy.ts +0 -7
  23. package/src/modules/dx/newsletterForm/newsletterForm.ts +20 -11
  24. package/src/modules/dx/sidebar/__tests__/mockSearchChangeEmptyEvent.ts +6 -0
  25. package/src/modules/dx/sidebar/__tests__/mockSearchChangeEvent.ts +527 -0
  26. package/src/modules/dx/sidebar/__tests__/sidebar.test.ts +210 -247
  27. package/src/modules/dx/sidebar/sidebar.css +40 -17
  28. package/src/modules/dx/sidebar/sidebar.html +58 -34
  29. package/src/modules/dx/sidebar/sidebar.stories.ts +54 -4
  30. package/src/modules/dx/sidebar/sidebar.ts +141 -73
  31. package/src/modules/dx/sidebarOld/__tests__/mockProps.ts +178 -0
  32. package/src/modules/dx/sidebarOld/__tests__/sidebarOld.test.ts +574 -0
  33. package/src/modules/dx/sidebarOld/sidebarOld.css +158 -0
  34. package/src/modules/dx/sidebarOld/sidebarOld.html +70 -0
  35. package/src/modules/dx/sidebarOld/sidebarOld.stories.ts +139 -0
  36. package/src/modules/dx/sidebarOld/sidebarOld.ts +187 -0
  37. package/src/modules/dx/sidebarSearch/__tests__/mockPreloadedState.ts +8843 -0
  38. package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +113 -0
  39. package/src/modules/dx/sidebarSearch/sidebarSearch.css +3 -0
  40. package/src/modules/dx/sidebarSearch/sidebarSearch.html +26 -0
  41. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +421 -0
  42. package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
  43. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +36 -0
  44. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +14 -0
  45. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +56 -0
  46. package/src/modules/dx/treeTile/__tests__/treeTile.test.ts +27 -38
  47. package/src/modules/dx/treeTile/treeTile.css +3 -19
  48. package/src/modules/dx/treeTile/treeTile.html +4 -1
  49. package/src/modules/dx/treeTile/treeTile.ts +2 -2
  50. package/src/modules/helpers/sidebar/sidebar.css +21 -0
  51. package/src/modules/utils/coveo/coveo.ts +69 -0
  52. package/src/modules/utils/headerBase/headerBase.ts +13 -2
  53. package/src/modules/utils/highlight/__tests__/highlight.test.ts +108 -0
  54. package/src/modules/utils/highlight/__tests__/mockProps.ts +45 -0
  55. package/src/modules/utils/highlight/highlight.ts +2 -1
  56. package/src/modules/utils/jest/jest.ts +35 -0
  57. package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
  58. package/src/modules/utils/recentSearches/recentSearches.ts +57 -0
  59. package/src/modules/utils/tests/tests.ts +18 -11
  60. package/src/modules/dx/feedbackBanner/__tests__/feedbackBanner.test.ts +0 -19
  61. package/src/modules/dx/feedbackBanner/feedbackBanner.html +0 -13
  62. package/src/modules/dx/feedbackBanner/feedbackBanner.ts +0 -3
@@ -0,0 +1,56 @@
1
+ import { LightningElement, api } from "lwc";
2
+ import cx from "classnames";
3
+ import { CoveoHighlights } from "typings/custom";
4
+
5
+ const toChunks = (value: string, highlights: CoveoHighlights) => {
6
+ if (!highlights || highlights.length < 1) {
7
+ return [
8
+ {
9
+ value,
10
+ id: 0
11
+ }
12
+ ];
13
+ }
14
+
15
+ let endIndex = 0;
16
+ return highlights.reduce((acc, { length, offset }) => {
17
+ const plainValue = value.slice(endIndex, offset);
18
+ const plainChunk = {
19
+ value: plainValue,
20
+ id: endIndex
21
+ };
22
+
23
+ endIndex = offset + length;
24
+ const boldValue = value.slice(offset, endIndex);
25
+ const boldChunk = {
26
+ value: boldValue,
27
+ class: "bold",
28
+ id: offset
29
+ };
30
+
31
+ return plainValue.length > 0
32
+ ? [...acc, plainChunk, boldChunk]
33
+ : [...acc, boldChunk];
34
+ }, []);
35
+ };
36
+
37
+ export default class SidebarSearchResult extends LightningElement {
38
+ @api description!: string;
39
+ @api descriptionHighlights!: CoveoHighlights;
40
+ @api href!: string;
41
+ @api selected!: boolean;
42
+ @api title!: string;
43
+ @api titleHighlights!: CoveoHighlights;
44
+
45
+ private get titleChunks() {
46
+ return toChunks(this.title, this.titleHighlights);
47
+ }
48
+
49
+ private get descriptionChunks() {
50
+ return toChunks(this.description, this.descriptionHighlights);
51
+ }
52
+
53
+ private get className(): string {
54
+ return cx("sidebar-item", this.selected && "sidebar-item-selected");
55
+ }
56
+ }
@@ -24,23 +24,20 @@ describe(TAG, () => {
24
24
  describe("render", () => {
25
25
  it("should render as a leaf tile", () => {
26
26
  const component = render({ treeNode: SINGLE_NODE, href: "/test" });
27
- const tileAnchor: HTMLElement = component.shadowRoot.querySelector(
28
- "a.tile"
29
- );
27
+ const tileAnchor: HTMLElement =
28
+ component.shadowRoot.querySelector("a.tile");
30
29
 
31
30
  expect(tileAnchor.getAttribute("href")).toBe("/test");
32
- expect(tileAnchor.classList).toHaveLength(1);
31
+ expect(tileAnchor.classList).toHaveLength(2);
33
32
  expect(tileAnchor).not.toBeNull();
34
33
  assertLevelAndLabel(tileAnchor);
35
34
 
36
- const arrowIcon: Icon = component.shadowRoot.querySelector(
37
- "dx-icon"
38
- );
35
+ const arrowIcon: Icon =
36
+ component.shadowRoot.querySelector("dx-icon");
39
37
  expect(arrowIcon).toBeNull();
40
38
 
41
- const typeBadge = component.shadowRoot.querySelector(
42
- "dx-type-badge"
43
- );
39
+ const typeBadge =
40
+ component.shadowRoot.querySelector("dx-type-badge");
44
41
  expect(typeBadge).toBeNull();
45
42
 
46
43
  return expect(component).toBeAccessible();
@@ -52,23 +49,20 @@ describe(TAG, () => {
52
49
  isRoot: true,
53
50
  hasChildren: true
54
51
  });
55
- const tileAnchor: HTMLElement = component.shadowRoot.querySelector(
56
- "a.tile.tile-root"
57
- );
52
+ const tileAnchor: HTMLElement =
53
+ component.shadowRoot.querySelector("a.tile.tile-root");
58
54
  expect(tileAnchor).not.toBeNull();
59
- expect(tileAnchor.classList).toHaveLength(2);
55
+ expect(tileAnchor.classList).toHaveLength(3);
60
56
  assertLevelAndLabel(tileAnchor);
61
57
 
62
- const arrowIcon: Icon = component.shadowRoot.querySelector(
63
- "dx-icon"
64
- )!;
58
+ const arrowIcon: Icon =
59
+ component.shadowRoot.querySelector("dx-icon")!;
65
60
  expect(arrowIcon).not.toBeNull();
66
61
  expect(arrowIcon.getAttribute("aria-expanded")).toBe("false");
67
62
  expect(arrowIcon.size).toBe("small");
68
63
 
69
- const typeBadge = component.shadowRoot.querySelector(
70
- "dx-type-badge"
71
- );
64
+ const typeBadge =
65
+ component.shadowRoot.querySelector("dx-type-badge");
72
66
  expect(typeBadge).toBeNull();
73
67
 
74
68
  return expect(component).toBeAccessible();
@@ -85,19 +79,17 @@ describe(TAG, () => {
85
79
  );
86
80
 
87
81
  expect(tileAnchor).not.toBeNull();
88
- expect(tileAnchor.classList).toHaveLength(2);
82
+ expect(tileAnchor.classList).toHaveLength(3);
89
83
  assertLevelAndLabel(tileAnchor);
90
84
 
91
- const arrowIcon: Icon = component.shadowRoot.querySelector(
92
- "dx-icon"
93
- );
85
+ const arrowIcon: Icon =
86
+ component.shadowRoot.querySelector("dx-icon");
94
87
  expect(arrowIcon).not.toBeNull();
95
88
  expect(arrowIcon.getAttribute("aria-expanded")).toBe("true");
96
89
  expect(arrowIcon.size).toBe("xsmall");
97
90
 
98
- const typeBadge = component.shadowRoot.querySelector(
99
- "dx-type-badge"
100
- );
91
+ const typeBadge =
92
+ component.shadowRoot.querySelector("dx-type-badge");
101
93
  expect(typeBadge).toBeNull();
102
94
 
103
95
  return expect(component).toBeAccessible();
@@ -109,19 +101,18 @@ describe(TAG, () => {
109
101
  isSelected: true
110
102
  });
111
103
  const tileAnchor: HTMLElement = component.shadowRoot.querySelector(
112
- "a.tile.tile-selected"
104
+ "a.tile.sidebar-item-selected"
113
105
  );
114
106
  expect(tileAnchor).not.toBeNull();
115
- expect(tileAnchor.classList).toHaveLength(2);
107
+ expect(tileAnchor.classList).toHaveLength(3);
116
108
 
117
109
  return expect(component).toBeAccessible();
118
110
  });
119
111
 
120
112
  it("should render a leaf tile with a valid brand method", () => {
121
113
  const component = render({ treeNode: SINGLE_NODE_WITH_METHOD });
122
- const typeBadge = component.shadowRoot.querySelector(
123
- "dx-type-badge"
124
- );
114
+ const typeBadge =
115
+ component.shadowRoot.querySelector("dx-type-badge");
125
116
 
126
117
  expect(typeBadge).not.toBeNull();
127
118
  expect(typeBadge.classList).toHaveLength(3);
@@ -140,9 +131,8 @@ describe(TAG, () => {
140
131
  const component = render({
141
132
  treeNode: { ...SINGLE_NODE_WITH_METHOD, method }
142
133
  });
143
- const typeBadge = component.shadowRoot.querySelector(
144
- "dx-type-badge"
145
- );
134
+ const typeBadge =
135
+ component.shadowRoot.querySelector("dx-type-badge");
146
136
 
147
137
  expect(typeBadge).not.toBeNull();
148
138
  expect(typeBadge.classList).toHaveLength(3);
@@ -163,9 +153,8 @@ describe(TAG, () => {
163
153
  isExpanded: true
164
154
  });
165
155
 
166
- const arrowIcon: HTMLElement = component.shadowRoot.querySelector(
167
- "dx-icon"
168
- );
156
+ const arrowIcon: HTMLElement =
157
+ component.shadowRoot.querySelector("dx-icon");
169
158
 
170
159
  const mockIconClick = jest.fn();
171
160
  component.addEventListener("iconclick", mockIconClick);
@@ -1,3 +1,6 @@
1
+ @import "helpers/reset";
2
+ @import "helpers/sidebar";
3
+
1
4
  :host {
2
5
  display: block;
3
6
 
@@ -22,11 +25,6 @@
22
25
  font-family: var(--dx-g-font-sans);
23
26
  font-size: 13px;
24
27
  line-height: var(--tile-height);
25
- padding: 6px var(--dx-g-spacing-md);
26
- }
27
-
28
- .tile:not(.tile-selected):hover {
29
- background-color: var(--dx-g-gray-90);
30
28
  }
31
29
 
32
30
  .tile-root {
@@ -37,20 +35,6 @@
37
35
  display: block;
38
36
  }
39
37
 
40
- .tile-selected {
41
- background: var(--dx-g-cloud-blue-vibrant-95);
42
- box-shadow: inset var(--dx-g-spacing-xs) 0 0 0 var(--dx-g-blue-vibrant-60);
43
- border-radius: 0;
44
- }
45
-
46
- .truncate {
47
- display: -webkit-box;
48
- overflow: hidden;
49
- word-break: break-word;
50
- -webkit-box-orient: vertical;
51
- -webkit-line-clamp: 2;
52
- }
53
-
54
38
  .tile-icon {
55
39
  align-items: center;
56
40
  height: var(--tile-height);
@@ -1,7 +1,10 @@
1
1
  <template>
2
2
  <a href={href} class={tileClass}>
3
3
  <div class="flex indentation" aria-level={tileAriaLevel}>
4
- <span class="tile-label truncate" title={treeNode.label}>
4
+ <span
5
+ class="tile-label sidebar-item-truncate-text"
6
+ title={treeNode.label}
7
+ >
5
8
  {treeNode.label}
6
9
  </span>
7
10
  <div class="flex tile-expand-icon tile-icon" if:true={hasChildren}>
@@ -44,10 +44,10 @@ export default class TreeTile extends LightningElement {
44
44
  }
45
45
 
46
46
  private get tileClass() {
47
- return cx("tile", {
47
+ return cx("tile", "sidebar-item", {
48
48
  "tile-root": this.isRoot,
49
49
  "tile-with-children": !this.isRoot && this.hasChildren,
50
- "tile-selected": this.isSelected
50
+ "sidebar-item-selected": this.isSelected
51
51
  });
52
52
  }
53
53
 
@@ -0,0 +1,21 @@
1
+ .sidebar-item-truncate-text {
2
+ display: -webkit-box;
3
+ overflow: hidden;
4
+ word-break: break-word;
5
+ -webkit-box-orient: vertical;
6
+ -webkit-line-clamp: 2;
7
+ }
8
+
9
+ .sidebar-item-selected {
10
+ background: var(--dx-g-cloud-blue-vibrant-95);
11
+ box-shadow: inset var(--dx-g-spacing-xs) 0 0 0 var(--dx-g-blue-vibrant-60);
12
+ border-radius: 0;
13
+ }
14
+
15
+ .sidebar-item {
16
+ padding: 6px var(--dx-g-spacing-md);
17
+ }
18
+
19
+ .sidebar-item:not(.sidebar-item-selected):hover {
20
+ background-color: var(--dx-g-gray-90);
21
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ buildSearchEngine as _buildSearchEngine,
3
+ SearchAppState,
4
+ SearchEngineOptions
5
+ } from "@coveo/headless";
6
+
7
+ export const buildSearchEngine = ({
8
+ organizationId,
9
+ preloadedState,
10
+ publicAccessToken,
11
+ searchHub,
12
+ searchPipeline
13
+ }: {
14
+ organizationId: string;
15
+ publicAccessToken: string;
16
+ searchPipeline?: string;
17
+ searchHub: string;
18
+ preloadedState?: SearchAppState;
19
+ }) => {
20
+ const search = searchPipeline
21
+ ? { pipeline: searchPipeline, searchHub }
22
+ : { searchHub };
23
+
24
+ const configuration = {
25
+ accessToken: publicAccessToken,
26
+ organizationId,
27
+ search
28
+ };
29
+
30
+ const engineConfiguration: SearchEngineOptions = preloadedState
31
+ ? { configuration, preloadedState }
32
+ : { configuration };
33
+
34
+ return _buildSearchEngine(engineConfiguration);
35
+ };
36
+
37
+ export const getSidebarSearchParams = () =>
38
+ new URLSearchParams(window.location.search).get("q");
39
+
40
+ const coveoOrganizationId = "coveosalesforcetestakshatha";
41
+
42
+ export const coveoHeaderConfig = {
43
+ coveoOrganizationId,
44
+ coveoPublicAccessToken: "xx8d9c92ef-1018-4d92-a3c7-647dff01c21c",
45
+ coveoSearchPipeline: "salesforcedevdoc",
46
+ coveoSearchHub: "salesforcedevdoc"
47
+ };
48
+
49
+ export const coveoXMLDocsConfig = {
50
+ coveoOrganizationId,
51
+ coveoPublicAccessToken: "xxfeb39525-e402-4e41-a870-7132441f7875",
52
+ coveoSearchHub: "Developer_Docs_SS",
53
+ coveoAdvancedQueryConfig: JSON.stringify({
54
+ objecttype: "HTDeveloperDocumentsC",
55
+ sflocale__c: "en_US",
56
+ sfrelease__c: "232.0",
57
+ sfdelivarable__c: "apexcode"
58
+ })
59
+ };
60
+
61
+ export const coveoSFDocsConfig = {
62
+ coveoOrganizationId,
63
+ coveoPublicAccessToken: "xxfeb39525-e402-4e41-a870-7132441f7875",
64
+ coveoSearchHub: "Developer_MdDocs_SS",
65
+ coveoAdvancedQueryConfig: JSON.stringify({
66
+ source: "Sitemap - Developer Docs",
67
+ path_segment_3: "pardot"
68
+ })
69
+ };
@@ -18,7 +18,14 @@ const VALID_BRANDS = [
18
18
  "service",
19
19
  "success"
20
20
  ];
21
- const SIGN_UP_LABEL = "Sign Up";
21
+
22
+ export const ANALYTICS_INFO = {
23
+ clickText: "Sign Up",
24
+ itemTitle: "Sign Up Header Button",
25
+ elementType: "button",
26
+ destinationType: "internal",
27
+ ctaClick: true
28
+ };
22
29
 
23
30
  export abstract class HeaderBase extends LightningElement {
24
31
  @api bailHref?: string | null = null;
@@ -33,6 +40,9 @@ export abstract class HeaderBase extends LightningElement {
33
40
  @api subtitle!: string;
34
41
  @api title: string = "Salesforce";
35
42
  @api version?: string | null = null;
43
+ @api bannerMarkup =
44
+ '<span><a href="https://forms.gle/TdSyKFPHXoBx7seM9" target="blank">Share your feedback</a>about our new site.</span>';
45
+
36
46
 
37
47
  @api
38
48
  get navItems() {
@@ -182,7 +192,8 @@ export abstract class HeaderBase extends LightningElement {
182
192
 
183
193
  private handleSignUpClick(e: PointerEvent) {
184
194
  track(e.currentTarget!, "custEv_signupStart", {
185
- clickText: SIGN_UP_LABEL
195
+ ...ANALYTICS_INFO,
196
+ clickUrl: this.signupLink,
186
197
  });
187
198
  }
188
199
 
@@ -0,0 +1,108 @@
1
+ import { createElement } from "lwc";
2
+ import CodeBlock from "dx/codeBlock";
3
+ import { highlightTerms } from "utils/highlight";
4
+ import { mockContent, allowedTags } from "./mockProps";
5
+
6
+ const container = document.createElement("div");
7
+
8
+ describe("highlight utility", () => {
9
+ afterEach(() => (container.innerHTML = ""));
10
+
11
+ it("highlights text that matches search criteria", () => {
12
+ container.innerHTML = mockContent;
13
+ highlightTerms(
14
+ container.querySelectorAll(allowedTags.join(",")),
15
+ "apex"
16
+ );
17
+ expect(container.querySelectorAll("mark")).toHaveLength(14);
18
+ });
19
+
20
+ it("doesn't highlight elements that contains dx/docs components", () => {
21
+ const searchTerm = "customreport";
22
+ const codeBlock = createElement("dx-code-block", { is: CodeBlock });
23
+ Object.assign(codeBlock, {
24
+ codeBlock: searchTerm,
25
+ language: "text"
26
+ });
27
+ container.appendChild(document.createElement("p"));
28
+ container.firstElementChild?.appendChild(codeBlock);
29
+ highlightTerms(
30
+ container.querySelectorAll(allowedTags.join(",")),
31
+ searchTerm
32
+ );
33
+ expect(container.querySelectorAll("mark")).toHaveLength(0);
34
+ expect(codeBlock.shadowRoot.innerHTML).toMatch(searchTerm); // sanity check
35
+ });
36
+
37
+ it("doesn't highlight terms separated by html tags", () => {
38
+ container.innerHTML = "<p>loadData<span>within</span></p>";
39
+ highlightTerms(
40
+ container.querySelectorAll(allowedTags.join(",")),
41
+ "loadDatawithin"
42
+ );
43
+ expect(container.querySelectorAll("mark")).toHaveLength(0);
44
+ });
45
+
46
+ it("escapes regex especial characters", () => {
47
+ container.innerHTML = mockContent;
48
+ highlightTerms(
49
+ container.querySelectorAll(allowedTags.join(",")),
50
+ "apex.+"
51
+ );
52
+ expect(container.querySelectorAll("mark")).toHaveLength(0);
53
+ });
54
+
55
+ it("cleans previous search result before searching for a new term", () => {
56
+ const firstSearch = "apex";
57
+ const secondSearch = "salesforce";
58
+
59
+ container.innerHTML = mockContent;
60
+
61
+ highlightTerms(
62
+ container.querySelectorAll(allowedTags.join(",")),
63
+ firstSearch
64
+ );
65
+ const firstMarks = container.querySelectorAll("mark");
66
+ expect(firstMarks).toHaveLength(14);
67
+ firstMarks.forEach(({ textContent }) =>
68
+ expect(textContent?.toLowerCase()).toBe(firstSearch)
69
+ );
70
+
71
+ highlightTerms(
72
+ container.querySelectorAll(allowedTags.join(",")),
73
+ secondSearch
74
+ );
75
+ const secondMarks = container.querySelectorAll("mark");
76
+ expect(secondMarks).toHaveLength(4);
77
+ secondMarks.forEach(({ textContent }) =>
78
+ expect(textContent?.toLowerCase()).toBe(secondSearch)
79
+ );
80
+ });
81
+
82
+ it("cleans all marks if the search term is empty string", () => {
83
+ const firstSearch = "apex";
84
+ const secondSearch = "";
85
+
86
+ container.innerHTML = mockContent;
87
+
88
+ highlightTerms(
89
+ container.querySelectorAll(allowedTags.join(",")),
90
+ firstSearch
91
+ );
92
+ const firstMarks = container.querySelectorAll("mark");
93
+ expect(firstMarks).toHaveLength(14);
94
+ firstMarks.forEach(({ textContent }) =>
95
+ expect(textContent?.toLowerCase()).toBe(firstSearch)
96
+ );
97
+
98
+ highlightTerms(
99
+ container.querySelectorAll(allowedTags.join(",")),
100
+ secondSearch
101
+ );
102
+ const secondMarks = container.querySelectorAll("mark");
103
+ expect(secondMarks).toHaveLength(0);
104
+ secondMarks.forEach(({ textContent }) =>
105
+ expect(textContent?.toLowerCase()).toBe(secondSearch)
106
+ );
107
+ });
108
+ });
@@ -0,0 +1,45 @@
1
+ export const mockContent = `<div><a name='apex_dev_guide'><!-- --></a><h1 class='helpHead1'>Apex Developer Guide</h1><div class='shortdesc'>Salesforce has changed the way organizations do business by moving enterprise
2
+ applications that were traditionally client-server-based into the Lightning Platform, an
3
+ on-demand, multitenant Web environment. This environment enables you to run and customize
4
+ applications, such as Salesforce Automation and Service &amp; Support, and build new custom
5
+ applications based on particular business needs. </div><div id='sfdc:seealso' class='related-links'>
6
+ <ul class='ullinks'>
7
+ <li class='link ulchildlink'>
8
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_intro_get_started.htm'>Getting Started with Apex</a></strong><br>
9
+ Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Lightning Platform server, in conjunction with calls to the API.</li>
10
+ <li class='link ulchildlink'>
11
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_writing.htm'>Writing Apex</a></strong><br>
12
+ Apex is like Java for Salesforce. It enables you to add and interact with data in the Lightning Platform persistence layer. It uses classes, data types, variables, and if-else statements. You can make it execute based on a condition, or have a block of code execute repeatedly.</li>
13
+ <li class='link ulchildlink'>
14
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_running.htm'>Running Apex</a></strong><br>
15
+ You can access many features of the Salesforce user interface programmatically in Apex, and you can integrate with external SOAP and REST Web services. You can run Apex code using a variety of mechanisms. Apex code runs in atomic transactions. </li>
16
+ <li class='link ulchildlink'>
17
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_debug_test_deploy.htm'>Debugging, Testing, and Deploying Apex</a></strong><br>
18
+ Develop your Apex code in a sandbox and debug it with the Developer Console and debug logs. Unit-test your code, then distribute it to customers using packages.</li>
19
+ <li class='link ulchildlink'>
20
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_reference.htm'>Apex Language Reference</a></strong><br>
21
+ This Apex reference goes into detail about DML statements and the built-in Apex classes and interfaces.</li>
22
+ <li class='link ulchildlink'>
23
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/apex_appendices.htm'>Appendices</a></strong><br>
24
+ </li>
25
+ <li class='link ulchildlink'>
26
+ <strong><a href='atlas.en-us.apexcode.meta/apexcode/glossary_apex.htm'>Glossary</a></strong><br>
27
+ </li>
28
+ </ul>
29
+ </div><!--DRC 228.7--></div>`;
30
+
31
+ export const allowedTags = [
32
+ "p",
33
+ ".p",
34
+ ".shortdesc",
35
+ "h1",
36
+ "h2",
37
+ "h3",
38
+ "h4",
39
+ "h5",
40
+ "h6",
41
+ "li",
42
+ "dl",
43
+ "th",
44
+ "td"
45
+ ];
@@ -3,7 +3,8 @@ import { createSearchRegExp } from "utils/regexps";
3
3
  const MARK_TAGS = /<\/?mark>/gi;
4
4
 
5
5
  export const highlightTerms = (
6
- elements: Array<HTMLElement>,
6
+ // eslint-disable-next-line no-undef
7
+ elements: NodeListOf<Element>,
7
8
  searchTerm: string
8
9
  ): void => {
9
10
  searchTerm = searchTerm.trim();
@@ -21,3 +21,38 @@ export const createMediaMock = (
21
21
  };
22
22
 
23
23
  export const matchMediaMock = createMediaMock();
24
+
25
+ export const mockLocalStorage = function () {
26
+ let store: { [key: string]: string } = {};
27
+ return {
28
+ getItem: function (key: string) {
29
+ return store[key] || null;
30
+ },
31
+ setItem: function (key: string, value: string) {
32
+ store[key] = value.toString();
33
+ },
34
+ removeItem: function (key: string) {
35
+ delete store[key];
36
+ },
37
+ clear: function () {
38
+ store = {};
39
+ }
40
+ };
41
+ };
42
+
43
+ export const mockQueryParameter = (q: string): void => {
44
+ global.window = Object.create(window);
45
+ const search = q ? `?q=${q}` : "";
46
+ const href = `http://dummy.com${search}`;
47
+ // @ts-ignore
48
+ delete window.location.value;
49
+ Object.defineProperty(window, "location", {
50
+ value: {
51
+ href,
52
+ search
53
+ },
54
+ writable: true
55
+ });
56
+ };
57
+
58
+ export const unmockQueryParameter = () => mockQueryParameter("");
@@ -0,0 +1,79 @@
1
+ import { RecentSearches, LOCAL_STORAGE_KEY, LIMIT } from "utils/recentSearches";
2
+
3
+ describe("recentSearches", () => {
4
+ beforeEach(() => {
5
+ window.localStorage.clear();
6
+ });
7
+
8
+ it("initializes correctly from local storage", () => {
9
+ const initial = ["test", "test1", "test2"];
10
+
11
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(initial));
12
+ const searches = new RecentSearches();
13
+
14
+ expect(searches.get()).toEqual(initial);
15
+ });
16
+
17
+ it("ignores exact duplicate additions", () => {
18
+ const searches = new RecentSearches();
19
+
20
+ const item = "test";
21
+
22
+ searches.add(item);
23
+ searches.add(item);
24
+
25
+ expect(searches.get().length).toEqual(1);
26
+ });
27
+
28
+ it("ignores duplicate additions with extraneous white space", () => {
29
+ const searches = new RecentSearches();
30
+
31
+ const item1 = "test";
32
+ const item2 = " test ";
33
+
34
+ searches.add(item1);
35
+ searches.add(item2);
36
+
37
+ expect(searches.get().length).toEqual(1);
38
+ expect(searches.get()[0]).toEqual(item1);
39
+ });
40
+
41
+ it("prioritizes the last searched item", () => {
42
+ const searches = new RecentSearches();
43
+
44
+ const item1 = "test1";
45
+ const item2 = "test2";
46
+
47
+ searches.add(item1);
48
+ searches.add(item2);
49
+
50
+ expect(searches.get().length).toEqual(2);
51
+
52
+ expect(searches.get()[0]).toEqual(item2);
53
+ expect(searches.get()[1]).toEqual(item1);
54
+
55
+ searches.add(item1);
56
+
57
+ expect(searches.get().length).toEqual(2);
58
+ expect(searches.get()[0]).toEqual(item1);
59
+ expect(searches.get()[1]).toEqual(item2);
60
+ });
61
+
62
+ it("will not exceed the limit", () => {
63
+ const searches = new RecentSearches();
64
+
65
+ Array.apply(null, Array(LIMIT + 1)).forEach((_, i) => {
66
+ searches.add(i.toString());
67
+ });
68
+
69
+ expect(searches.get().length).toEqual(LIMIT);
70
+ });
71
+
72
+ it("doesn't add a bogus value", () => {
73
+ const searches = new RecentSearches();
74
+
75
+ [null, "", " ", " ", 5].forEach((v) => searches.add(v));
76
+
77
+ expect(searches.get().length).toEqual(0);
78
+ });
79
+ });