@salesforcedevs/dx-components 0.10.1-alpha.93 → 0.10.1-alpha.97
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 +2 -2
- package/src/assets/svg/docs-empty-state-small.svg +72 -0
- package/src/modules/dx/emptyState/__tests__/emptyState.test.ts +29 -9
- package/src/modules/dx/emptyState/emptyState.css +14 -0
- package/src/modules/dx/emptyState/emptyState.html +2 -2
- package/src/modules/dx/emptyState/emptyState.stories.ts +8 -0
- package/src/modules/dx/emptyState/emptyState.ts +14 -0
- package/src/modules/dx/headerNav/__tests__/headerNav.test.ts +2 -4
- package/src/modules/dx/headerNav/headerNav.html +8 -6
- package/src/modules/dx/headerNav/headerNav.ts +2 -2
- package/src/modules/dx/newsletterForm/newsletterForm.html +5 -3
- package/src/modules/dx/sidebar/sidebar.html +1 -0
- package/src/modules/dx/sidebar/sidebar.stories.ts +1 -22
- package/src/modules/dx/sidebar/sidebar.ts +6 -2
- package/src/modules/dx/sidebarSearch/__tests__/preloadedState.ts +3459 -0
- package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +44 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +61 -4161
- package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +1 -1
- package/src/modules/dx/{headerNavOption/__tests__/headerNavOption.test.ts → tab/__tests__/tab.test.ts} +40 -24
- package/src/modules/dx/{headerNavOption/headerNavOption.css → tab/tab.css} +24 -9
- package/src/modules/dx/{headerNavOption/headerNavOption.html → tab/tab.html} +5 -1
- package/src/modules/dx/tab/tab.stories.ts +57 -0
- package/src/modules/dx/{headerNavOption/headerNavOption.ts → tab/tab.ts} +26 -5
- package/src/modules/utils/coveo/coveo.ts +46 -11
- package/src/modules/utils/normalizers/normalizers.ts +3 -0
- package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
- package/src/modules/utils/recentSearches/recentSearches.ts +7 -16
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import SidebarSearchResult from "dx/sidebarSearchResult";
|
|
2
|
+
import { createRenderComponent } from "utils/tests";
|
|
3
|
+
|
|
4
|
+
const TAG = "dx-sidebar-search-result";
|
|
5
|
+
const render = createRenderComponent(TAG, SidebarSearchResult);
|
|
6
|
+
|
|
7
|
+
const PROPS = {
|
|
8
|
+
description: "test description here",
|
|
9
|
+
descriptionHighlights: [{ length: 5, offset: 5 }],
|
|
10
|
+
href: "/test",
|
|
11
|
+
selected: false,
|
|
12
|
+
title: "test title here",
|
|
13
|
+
titleHighlights: [{ length: 5, offset: 5 }]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe(TAG, () => {
|
|
17
|
+
it("renders a result with the correct highlighted content", () => {
|
|
18
|
+
const element = render(PROPS);
|
|
19
|
+
|
|
20
|
+
const titleBolds = element.shadowRoot.querySelectorAll(".title .bold");
|
|
21
|
+
expect(titleBolds.length).toEqual(1);
|
|
22
|
+
expect(titleBolds[0].textContent).toEqual("title");
|
|
23
|
+
|
|
24
|
+
const descriptionBolds =
|
|
25
|
+
element.shadowRoot.querySelectorAll(".description .bold");
|
|
26
|
+
expect(descriptionBolds.length).toEqual(1);
|
|
27
|
+
expect(descriptionBolds[0].textContent).toEqual("descr");
|
|
28
|
+
|
|
29
|
+
return expect(element).toBeAccessible();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders a result as selected when specified", () => {
|
|
33
|
+
const element = render({ ...PROPS, selected: true });
|
|
34
|
+
|
|
35
|
+
const selected = element.shadowRoot.querySelector(
|
|
36
|
+
".sidebar-item-selected"
|
|
37
|
+
);
|
|
38
|
+
expect(selected).not.toBeNull();
|
|
39
|
+
|
|
40
|
+
return expect(element).toBeAccessible();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("doesn't add bold class when there is no highlighted term", () => {
|
|
44
|
+
const element = render({
|
|
45
|
+
...PROPS,
|
|
46
|
+
descriptionHighlights: [],
|
|
47
|
+
titleHighlights: []
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const selected = element.shadowRoot.querySelector(".bold");
|
|
51
|
+
expect(selected).toBeNull();
|
|
52
|
+
|
|
53
|
+
return expect(element).toBeAccessible();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Tab from "dx/tab";
|
|
2
2
|
import Icon from "dx/icon";
|
|
3
|
+
import { track } from "dx/instrumentation";
|
|
3
4
|
import { createRenderComponent } from "utils/tests";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
jest.mock("dx/instrumentation");
|
|
7
|
+
|
|
8
|
+
const TAG = "dx-tab";
|
|
9
|
+
const render = createRenderComponent(TAG, Tab);
|
|
7
10
|
|
|
8
11
|
const PROPS = {
|
|
9
12
|
label: "documentation",
|
|
@@ -14,9 +17,8 @@ describe(TAG, () => {
|
|
|
14
17
|
it("renders a single link with a label", () => {
|
|
15
18
|
const element = render(PROPS);
|
|
16
19
|
|
|
17
|
-
const links: Array<HTMLAnchorElement> =
|
|
18
|
-
"a"
|
|
19
|
-
);
|
|
20
|
+
const links: Array<HTMLAnchorElement> =
|
|
21
|
+
element.shadowRoot.querySelectorAll("a");
|
|
20
22
|
|
|
21
23
|
expect(links.length).toEqual(1);
|
|
22
24
|
const [singleLink] = links;
|
|
@@ -53,9 +55,8 @@ describe(TAG, () => {
|
|
|
53
55
|
const label = "test button";
|
|
54
56
|
const element = render({ label });
|
|
55
57
|
|
|
56
|
-
const buttons: Array<HTMLButtonElement> =
|
|
57
|
-
"button"
|
|
58
|
-
);
|
|
58
|
+
const buttons: Array<HTMLButtonElement> =
|
|
59
|
+
element.shadowRoot.querySelectorAll("button");
|
|
59
60
|
|
|
60
61
|
expect(buttons).toHaveLength(1);
|
|
61
62
|
const [singleButton] = buttons;
|
|
@@ -66,6 +67,23 @@ describe(TAG, () => {
|
|
|
66
67
|
|
|
67
68
|
const span: HTMLElement = singleButton.querySelector("span")!;
|
|
68
69
|
expect(span.textContent).toBe(label);
|
|
70
|
+
|
|
71
|
+
return expect(element).toBeAccessible();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders a button with an icon if useIcon is passed as true", () => {
|
|
75
|
+
const label = "test button";
|
|
76
|
+
const element = render({ label, useIcon: true });
|
|
77
|
+
|
|
78
|
+
const buttons: Array<HTMLButtonElement> =
|
|
79
|
+
element.shadowRoot.querySelectorAll("button");
|
|
80
|
+
|
|
81
|
+
expect(buttons).toHaveLength(1);
|
|
82
|
+
const [singleButton] = buttons;
|
|
83
|
+
|
|
84
|
+
const span: HTMLElement = singleButton.querySelector("span")!;
|
|
85
|
+
expect(span.textContent).toBe(label);
|
|
86
|
+
|
|
69
87
|
const icon: Element & Icon = singleButton.querySelector("dx-icon")!;
|
|
70
88
|
expect(icon).not.toBeNull();
|
|
71
89
|
expect(icon.size).toBe("small");
|
|
@@ -75,11 +93,10 @@ describe(TAG, () => {
|
|
|
75
93
|
|
|
76
94
|
it("renders a button with label only if variant is small", () => {
|
|
77
95
|
const label = "test button";
|
|
78
|
-
const element = render({ label, variant: "small" });
|
|
96
|
+
const element = render({ label, variant: "small", useIcon: "true" });
|
|
79
97
|
|
|
80
|
-
const button: HTMLButtonElement =
|
|
81
|
-
"button"
|
|
82
|
-
);
|
|
98
|
+
const button: HTMLButtonElement =
|
|
99
|
+
element.shadowRoot.querySelector("button");
|
|
83
100
|
|
|
84
101
|
expect(button.classList).toHaveLength(2);
|
|
85
102
|
expect(button.classList).toContain("nav-list-item");
|
|
@@ -95,19 +112,18 @@ describe(TAG, () => {
|
|
|
95
112
|
return expect(element).toBeAccessible();
|
|
96
113
|
});
|
|
97
114
|
|
|
98
|
-
it("
|
|
99
|
-
const
|
|
100
|
-
const element = render({
|
|
101
|
-
...PROPS,
|
|
102
|
-
onclick: sendGtmMock
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const link = element.shadowRoot.querySelector("a");
|
|
115
|
+
it("calls track when the link is clicked", () => {
|
|
116
|
+
const element = render(PROPS);
|
|
106
117
|
|
|
118
|
+
const link: HTMLElement = element.shadowRoot.querySelector("a");
|
|
119
|
+
expect(link).not.toBeNull();
|
|
107
120
|
link.click();
|
|
108
121
|
|
|
109
|
-
expect(
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
expect(track).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(track).toBeCalledWith(link, "custEv_topNavLinkClick", {
|
|
124
|
+
navHeading: PROPS.label,
|
|
125
|
+
clickText: PROPS.href,
|
|
126
|
+
pageLocation: window.location.pathname
|
|
127
|
+
});
|
|
112
128
|
});
|
|
113
129
|
});
|
|
@@ -5,7 +5,10 @@ a {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
.nav-list-item {
|
|
8
|
-
--horizontal-spacing: var(
|
|
8
|
+
--horizontal-spacing: var(
|
|
9
|
+
--dx-c-tab-horizontal-spacing,
|
|
10
|
+
var(--dx-g-spacing-md)
|
|
11
|
+
);
|
|
9
12
|
|
|
10
13
|
position: relative;
|
|
11
14
|
display: flex;
|
|
@@ -50,12 +53,15 @@ button.nav-list-item_small > dx-icon {
|
|
|
50
53
|
.nav-list-item_default:hover,
|
|
51
54
|
.nav-list-item_default:active,
|
|
52
55
|
:host([aria-expanded="true"]) button.nav-list-item_default {
|
|
53
|
-
background: var(
|
|
56
|
+
background: var(
|
|
57
|
+
--dx-g-brand-current-button-color-background-inactive,
|
|
58
|
+
var(--dx-g-brand-default-button-color-background-inactive)
|
|
59
|
+
);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
button.nav-list-item:hover >
|
|
57
|
-
button.nav-list-item:active >
|
|
58
|
-
:host([aria-expanded="true"]) button.nav-list-item >
|
|
62
|
+
button.nav-list-item:hover > dx-icon,
|
|
63
|
+
button.nav-list-item:active > dx-icon,
|
|
64
|
+
:host([aria-expanded="true"]) button.nav-list-item > dx-icon {
|
|
59
65
|
transform: rotateX(180deg);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -74,18 +80,27 @@ button.nav-list-item:active > *:last-child,
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
.nav-list-item_default.state-active {
|
|
77
|
-
background: var(
|
|
78
|
-
|
|
83
|
+
background: var(
|
|
84
|
+
--dx-g-brand-current-button-color-background-active,
|
|
85
|
+
var(--dx-g-brand-default-button-color-background-active)
|
|
86
|
+
);
|
|
87
|
+
color: var(--dx-g-brand-current-color, var(--dx-g-brand-default-color));
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
@media (max-width: 1160px) {
|
|
82
91
|
.nav-list-item_small {
|
|
83
|
-
--horizontal-spacing: var(
|
|
92
|
+
--horizontal-spacing: var(
|
|
93
|
+
--dx-c-tab-horizontal-spacing,
|
|
94
|
+
var(--dx-g-spacing-smd)
|
|
95
|
+
);
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
@media (max-width: 1090px) {
|
|
88
100
|
.nav-list-item_small {
|
|
89
|
-
--horizontal-spacing: var(
|
|
101
|
+
--horizontal-spacing: var(
|
|
102
|
+
--dx-c-tab-horizontal-spacing,
|
|
103
|
+
var(--dx-g-spacing-sm)
|
|
104
|
+
);
|
|
90
105
|
}
|
|
91
106
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { html } from "lit-html";
|
|
2
|
+
import { TabVariant } from "typings/custom";
|
|
3
|
+
|
|
4
|
+
type StoryArgs = {
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
label: string;
|
|
7
|
+
useIcon: boolean;
|
|
8
|
+
variant: TabVariant;
|
|
9
|
+
};
|
|
10
|
+
const VARIANT_OPTIONS: Array<TabVariant> = ["default", "small"];
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
title: "dx/dx-tab",
|
|
14
|
+
component: "dx-tab",
|
|
15
|
+
argTypes: {
|
|
16
|
+
variant: {
|
|
17
|
+
control: {
|
|
18
|
+
type: "select",
|
|
19
|
+
options: VARIANT_OPTIONS
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const Template = ({ variant, label, isActive, useIcon }: StoryArgs) => html`
|
|
26
|
+
<dx-tab
|
|
27
|
+
label="${label}"
|
|
28
|
+
is-active="${isActive}"
|
|
29
|
+
variant="${variant}"
|
|
30
|
+
use-icon="${useIcon}"
|
|
31
|
+
></dx-tab>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export const Base = (args: StoryArgs) => Template(args);
|
|
35
|
+
Base.args = {
|
|
36
|
+
isActive: false,
|
|
37
|
+
useIcon: false,
|
|
38
|
+
label: "Banana",
|
|
39
|
+
variant: VARIANT_OPTIONS[0]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const TabGroup = ({ variant }: { variant: TabVariant }) => html`
|
|
43
|
+
<style>
|
|
44
|
+
.container {
|
|
45
|
+
display: flex;
|
|
46
|
+
--dx-c-tab-horizontal-spacing: var(--dx-g-spacing-xs);
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
<nav class="container">
|
|
50
|
+
<dx-tab label="Banana" variant="${variant}"></dx-tab>
|
|
51
|
+
<dx-tab label="Apple" variant="${variant}"></dx-tab>
|
|
52
|
+
</nav>
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
TabGroup.args = {
|
|
56
|
+
variant: VARIANT_OPTIONS[0]
|
|
57
|
+
};
|
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
2
|
import { track } from "dx/instrumentation";
|
|
3
3
|
import cx from "classnames";
|
|
4
|
-
import {
|
|
4
|
+
import { TabVariant } from "typings/custom";
|
|
5
|
+
import { normalizeBoolean } from "utils/normalizers";
|
|
5
6
|
|
|
6
|
-
export default class
|
|
7
|
-
@api isActive: boolean = false;
|
|
7
|
+
export default class Tab extends LightningElement {
|
|
8
8
|
@api label: string | null = null;
|
|
9
9
|
@api href: string | null = null;
|
|
10
|
-
@api variant:
|
|
10
|
+
@api variant: TabVariant = "default";
|
|
11
|
+
|
|
12
|
+
@api
|
|
13
|
+
get isActive() {
|
|
14
|
+
return this._isActive;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
set isActive(value) {
|
|
18
|
+
this._isActive = normalizeBoolean(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@api
|
|
22
|
+
get useIcon() {
|
|
23
|
+
return this._useIcon;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set useIcon(value) {
|
|
27
|
+
this._useIcon = normalizeBoolean(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_useIcon = false;
|
|
31
|
+
_isActive = false;
|
|
11
32
|
|
|
12
33
|
private sendGtm(e: PointerEvent) {
|
|
13
34
|
track(e.currentTarget!, "custEv_topNavLinkClick", {
|
|
14
35
|
navHeading: this.label || undefined,
|
|
15
36
|
clickText: this.href || undefined,
|
|
16
|
-
pageLocation: window.location.pathname
|
|
37
|
+
pageLocation: window.location.pathname
|
|
17
38
|
});
|
|
18
39
|
}
|
|
19
40
|
|
|
@@ -1,25 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildSearchEngine as _buildSearchEngine,
|
|
3
|
+
SearchAppState,
|
|
4
|
+
SearchEngineOptions
|
|
5
|
+
} from "@coveo/headless";
|
|
2
6
|
|
|
3
7
|
export const buildSearchEngine = ({
|
|
4
8
|
organizationId,
|
|
9
|
+
preloadedState,
|
|
5
10
|
publicAccessToken,
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
searchHub,
|
|
12
|
+
searchPipeline
|
|
8
13
|
}: {
|
|
9
14
|
organizationId: string;
|
|
10
15
|
publicAccessToken: string;
|
|
11
16
|
searchPipeline: string;
|
|
12
17
|
searchHub: string;
|
|
18
|
+
preloadedState: SearchAppState;
|
|
13
19
|
}) => {
|
|
14
|
-
const search =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
22
35
|
};
|
|
23
36
|
|
|
24
37
|
export const getSidebarSearchParams = () =>
|
|
25
38
|
new URLSearchParams(window.location.search).get("q");
|
|
39
|
+
|
|
40
|
+
export const coveoXMLDocsConfig = {
|
|
41
|
+
coveoOrganizationId: "coveosalesforcetestakshatha",
|
|
42
|
+
coveoPublicAccessToken: "xxfeb39525-e402-4e41-a870-7132441f7875",
|
|
43
|
+
coveoSearchHub: "Developer_Docs_SS",
|
|
44
|
+
coveoAdvancedQueryConfig: JSON.stringify({
|
|
45
|
+
objecttype: "HTDeveloperDocumentsC",
|
|
46
|
+
sflanguage: "en_US",
|
|
47
|
+
sfrelease__c: "232.0",
|
|
48
|
+
sfdelivarable__c: "apexcode"
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const coveoSFDocsConfig = {
|
|
53
|
+
coveoOrganizationId: "coveosalesforcetestakshatha",
|
|
54
|
+
coveoPublicAccessToken: "xxfeb39525-e402-4e41-a870-7132441f7875",
|
|
55
|
+
coveoSearchHub: "Developer_MdDocs_SS",
|
|
56
|
+
coveoAdvancedQueryConfig: JSON.stringify({
|
|
57
|
+
source: "Sitemap - Developer Docs",
|
|
58
|
+
path_segment_3: "pardot"
|
|
59
|
+
})
|
|
60
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
export const LOCAL_STORAGE_KEY = "dx-sidebar-recent-searches";
|
|
2
2
|
|
|
3
|
-
const LIMIT = 5;
|
|
3
|
+
export const LIMIT = 5;
|
|
4
4
|
|
|
5
5
|
export class RecentSearches {
|
|
6
6
|
items: string[] = [];
|
|
@@ -10,16 +10,16 @@ export class RecentSearches {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
get() {
|
|
13
|
-
return this.items
|
|
13
|
+
return this.items;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
add(item: string | undefined) {
|
|
17
|
-
if (!item) {
|
|
17
|
+
if (!item || typeof item !== "string") {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const normalized = item.trim();
|
|
21
21
|
if (normalized === "") {
|
|
22
|
-
return
|
|
22
|
+
return;
|
|
23
23
|
}
|
|
24
24
|
this._prepend(normalized);
|
|
25
25
|
this._removeDuplicates(normalized);
|
|
@@ -30,7 +30,7 @@ export class RecentSearches {
|
|
|
30
30
|
// private
|
|
31
31
|
_init() {
|
|
32
32
|
this.items = JSON.parse(
|
|
33
|
-
window.localStorage.getItem(
|
|
33
|
+
window.localStorage.getItem(LOCAL_STORAGE_KEY) || "[]"
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -50,17 +50,8 @@ export class RecentSearches {
|
|
|
50
50
|
|
|
51
51
|
_addToStorage() {
|
|
52
52
|
window.localStorage.setItem(
|
|
53
|
-
|
|
53
|
+
LOCAL_STORAGE_KEY,
|
|
54
54
|
JSON.stringify(this.items)
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
_isDuplicate(searchItem: string) {
|
|
59
|
-
return (
|
|
60
|
-
this.items.filter(
|
|
61
|
-
(item: string) =>
|
|
62
|
-
item.toLowerCase() === searchItem.toLowerCase()
|
|
63
|
-
).length > 0
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
57
|
}
|