@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.
- package/lwc.config.json +2 -1
- package/package.json +3 -3
- package/src/assets/svg/sidebar-loading.svg +34 -0
- package/src/modules/dx/banner/__tests__/banner.test.ts +31 -0
- package/src/modules/dx/{feedbackBanner/feedbackBanner.css → banner/banner.css} +6 -6
- package/src/modules/dx/banner/banner.html +13 -0
- package/src/modules/dx/banner/banner.ts +14 -0
- package/src/modules/dx/dropdown/dropdown.html +2 -0
- package/src/modules/dx/dropdown/dropdown.ts +4 -2
- package/src/modules/dx/dropdownOption/__tests__/dropdownOption.test.ts +32 -2
- package/src/modules/dx/dropdownOption/dropdownOption.ts +9 -6
- package/src/modules/dx/footer/footer.ts +13 -1
- package/src/modules/dx/header/__tests__/header.test.ts +38 -21
- package/src/modules/dx/header/__tests__/mockProps.ts +29 -37
- package/src/modules/dx/header/header.html +4 -1
- package/src/modules/dx/header/header.stories.ts +30 -0
- package/src/modules/dx/header/header.ts +4 -1
- package/src/modules/dx/headerNav/headerNav.html +1 -0
- package/src/modules/dx/headerSearch/__tests__/headerSearch.test.ts +24 -39
- package/src/modules/dx/headerSearch/__tests__/mockProps.ts +2 -7
- package/src/modules/dx/headerSearch/headerSearch.ts +13 -19
- package/src/modules/dx/lazy/lazy.ts +0 -7
- package/src/modules/dx/newsletterForm/newsletterForm.ts +20 -11
- package/src/modules/dx/sidebar/__tests__/mockSearchChangeEmptyEvent.ts +6 -0
- package/src/modules/dx/sidebar/__tests__/mockSearchChangeEvent.ts +527 -0
- package/src/modules/dx/sidebar/__tests__/sidebar.test.ts +210 -247
- package/src/modules/dx/sidebar/sidebar.css +40 -17
- package/src/modules/dx/sidebar/sidebar.html +58 -34
- package/src/modules/dx/sidebar/sidebar.stories.ts +54 -4
- package/src/modules/dx/sidebar/sidebar.ts +141 -73
- package/src/modules/dx/sidebarOld/__tests__/mockProps.ts +178 -0
- package/src/modules/dx/sidebarOld/__tests__/sidebarOld.test.ts +574 -0
- package/src/modules/dx/sidebarOld/sidebarOld.css +158 -0
- package/src/modules/dx/sidebarOld/sidebarOld.html +70 -0
- package/src/modules/dx/sidebarOld/sidebarOld.stories.ts +139 -0
- package/src/modules/dx/sidebarOld/sidebarOld.ts +187 -0
- package/src/modules/dx/sidebarSearch/__tests__/mockPreloadedState.ts +8843 -0
- package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +113 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.css +3 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.html +26 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +421 -0
- package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +36 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +14 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +56 -0
- package/src/modules/dx/treeTile/__tests__/treeTile.test.ts +27 -38
- package/src/modules/dx/treeTile/treeTile.css +3 -19
- package/src/modules/dx/treeTile/treeTile.html +4 -1
- package/src/modules/dx/treeTile/treeTile.ts +2 -2
- package/src/modules/helpers/sidebar/sidebar.css +21 -0
- package/src/modules/utils/coveo/coveo.ts +69 -0
- package/src/modules/utils/headerBase/headerBase.ts +13 -2
- package/src/modules/utils/highlight/__tests__/highlight.test.ts +108 -0
- package/src/modules/utils/highlight/__tests__/mockProps.ts +45 -0
- package/src/modules/utils/highlight/highlight.ts +2 -1
- package/src/modules/utils/jest/jest.ts +35 -0
- package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
- package/src/modules/utils/recentSearches/recentSearches.ts +57 -0
- package/src/modules/utils/tests/tests.ts +18 -11
- package/src/modules/dx/feedbackBanner/__tests__/feedbackBanner.test.ts +0 -19
- package/src/modules/dx/feedbackBanner/feedbackBanner.html +0 -13
- package/src/modules/dx/feedbackBanner/feedbackBanner.ts +0 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createElement } from "lwc";
|
|
2
|
+
import SidebarSearch from "dx/sidebarSearch";
|
|
3
|
+
import { createRenderComponent, renderLightningElement } from "utils/tests";
|
|
4
|
+
import { coveoXMLDocsConfig } from "utils/coveo";
|
|
5
|
+
import { mockQueryParameter, unmockQueryParameter } from "utils/jest";
|
|
6
|
+
|
|
7
|
+
const TAG = "dx-sidebar-search";
|
|
8
|
+
const render = createRenderComponent(TAG, SidebarSearch);
|
|
9
|
+
|
|
10
|
+
const PROPS = coveoXMLDocsConfig;
|
|
11
|
+
const QUERY = "test";
|
|
12
|
+
|
|
13
|
+
describe(TAG, () => {
|
|
14
|
+
afterEach(unmockQueryParameter);
|
|
15
|
+
|
|
16
|
+
it("renders with the correct initial empty query", () => {
|
|
17
|
+
const element = render(PROPS);
|
|
18
|
+
|
|
19
|
+
const input = element.shadowRoot.querySelector("dx-input");
|
|
20
|
+
|
|
21
|
+
expect(input.value).toEqual("");
|
|
22
|
+
|
|
23
|
+
return expect(element).toBeAccessible();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders with the correct initial query", () => {
|
|
27
|
+
mockQueryParameter(QUERY);
|
|
28
|
+
|
|
29
|
+
const element = render(PROPS);
|
|
30
|
+
|
|
31
|
+
const input = element.shadowRoot.querySelector("dx-input");
|
|
32
|
+
|
|
33
|
+
expect(input.value).toEqual(QUERY);
|
|
34
|
+
|
|
35
|
+
return expect(element).toBeAccessible();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("highlighting", () => {
|
|
39
|
+
afterEach(unmockQueryParameter);
|
|
40
|
+
|
|
41
|
+
it("triggers on initial render when a search value exists", () => {
|
|
42
|
+
mockQueryParameter(QUERY);
|
|
43
|
+
const element = createElement(TAG, { is: SidebarSearch });
|
|
44
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
45
|
+
renderLightningElement(element, PROPS);
|
|
46
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
type: "highlightedtermchange",
|
|
49
|
+
detail: QUERY
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not trigger on initial render when a search value does not exist", () => {
|
|
55
|
+
const element = createElement(TAG, { is: SidebarSearch });
|
|
56
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
57
|
+
renderLightningElement(element, PROPS);
|
|
58
|
+
expect(dispatchEventSpy).not.toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({ type: "highlightedtermchange" })
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("triggers on input change with non-empty value", () => {
|
|
64
|
+
const element = render(PROPS);
|
|
65
|
+
const dxInput = element.shadowRoot.querySelector("dx-input");
|
|
66
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
67
|
+
|
|
68
|
+
dxInput.dispatchEvent(new CustomEvent("change", { detail: QUERY }));
|
|
69
|
+
|
|
70
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
type: "highlightedtermchange",
|
|
73
|
+
detail: QUERY
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("triggers on input change with empty value", () => {
|
|
79
|
+
const element = render(PROPS);
|
|
80
|
+
const dxInput = element.shadowRoot.querySelector("dx-input");
|
|
81
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
82
|
+
|
|
83
|
+
dxInput.dispatchEvent(new CustomEvent("change", { detail: "" }));
|
|
84
|
+
|
|
85
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
type: "highlightedtermchange",
|
|
88
|
+
detail: ""
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// git("correctly consumes a valid session storage object", () => {
|
|
96
|
+
// mockQueryParameter(mockPreloadedState.query.q);
|
|
97
|
+
|
|
98
|
+
// window.sessionStorage.setItem(
|
|
99
|
+
// SESSION_KEY,
|
|
100
|
+
// JSON.stringify(mockPreloadedState)
|
|
101
|
+
// );
|
|
102
|
+
|
|
103
|
+
// const mockOnChange = jest.fn();
|
|
104
|
+
|
|
105
|
+
// const element = render(PROPS);
|
|
106
|
+
// element.addEventListener("change", mockOnChange);
|
|
107
|
+
|
|
108
|
+
// return Promise.resolve().then(() => {
|
|
109
|
+
// expect(mockOnChange).toHaveBeenCalledTimes(1);
|
|
110
|
+
|
|
111
|
+
// expect(element).toBeAccessible();
|
|
112
|
+
// });
|
|
113
|
+
// });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<dx-dropdown
|
|
3
|
+
aria-label="Recent Searches"
|
|
4
|
+
open={isDropdownOpen}
|
|
5
|
+
options={recentSearches}
|
|
6
|
+
onchange={onClickRecentSearch}
|
|
7
|
+
onrequestclose={onDropdownRequestClose}
|
|
8
|
+
full-width
|
|
9
|
+
>
|
|
10
|
+
<dx-input
|
|
11
|
+
class="search-box"
|
|
12
|
+
type="search"
|
|
13
|
+
placeholder="Search this list..."
|
|
14
|
+
role="search"
|
|
15
|
+
aria-label="Content Search"
|
|
16
|
+
icon-symbol="search"
|
|
17
|
+
size="small"
|
|
18
|
+
value={value}
|
|
19
|
+
onchange={onInputChange}
|
|
20
|
+
onfocus={onInputFocus}
|
|
21
|
+
onclear={onInputClear}
|
|
22
|
+
shortcut-key="j"
|
|
23
|
+
clearable
|
|
24
|
+
></dx-input>
|
|
25
|
+
</dx-dropdown>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { LightningElement, api } from "lwc";
|
|
2
|
+
import debounce from "debounce";
|
|
3
|
+
import {
|
|
4
|
+
Result,
|
|
5
|
+
ResultList,
|
|
6
|
+
Unsubscribe,
|
|
7
|
+
ResultsPerPage,
|
|
8
|
+
SearchBox,
|
|
9
|
+
buildResultList,
|
|
10
|
+
buildResultsPerPage,
|
|
11
|
+
buildSearchBox,
|
|
12
|
+
loadAdvancedSearchQueryActions,
|
|
13
|
+
SearchEngine,
|
|
14
|
+
SearchAppState,
|
|
15
|
+
loadQueryActions
|
|
16
|
+
} from "@coveo/headless";
|
|
17
|
+
import { buildSearchEngine, getSidebarSearchParams } from "utils/coveo";
|
|
18
|
+
import { RecentSearches } from "utils/recentSearches";
|
|
19
|
+
import { toJson } from "utils/normalizers";
|
|
20
|
+
import {
|
|
21
|
+
Option,
|
|
22
|
+
PopoverRequestCloseType,
|
|
23
|
+
SidebarSearchResult
|
|
24
|
+
} from "typings/custom";
|
|
25
|
+
|
|
26
|
+
export const SESSION_KEY = "dx-sidebar-search-state";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_RESULT_COUNT = 20;
|
|
29
|
+
const SEARCH_DEBOUNCE_DELAY = 780;
|
|
30
|
+
|
|
31
|
+
const UserRecentSearches = new RecentSearches();
|
|
32
|
+
|
|
33
|
+
const normalizeCoveoAdvancedQueryValue = (version: string): string =>
|
|
34
|
+
version.split(".").join("\\.");
|
|
35
|
+
|
|
36
|
+
export default class SidebarSearch extends LightningElement {
|
|
37
|
+
@api coveoOrganizationId!: string;
|
|
38
|
+
@api coveoPublicAccessToken!: string;
|
|
39
|
+
@api coveoSearchHub!: string;
|
|
40
|
+
@api
|
|
41
|
+
get coveoAdvancedQueryConfig(): { [key: string]: any } {
|
|
42
|
+
return this._coveoAdvancedQueryConfig;
|
|
43
|
+
}
|
|
44
|
+
set coveoAdvancedQueryConfig(value) {
|
|
45
|
+
const jsonValue = toJson(value);
|
|
46
|
+
const hasChanged =
|
|
47
|
+
this._coveoAdvancedQueryConfig &&
|
|
48
|
+
Object.entries(jsonValue).some(
|
|
49
|
+
([key, val]: [string, any]) =>
|
|
50
|
+
this._coveoAdvancedQueryConfig[key] !== val
|
|
51
|
+
);
|
|
52
|
+
this._coveoAdvancedQueryConfig = jsonValue;
|
|
53
|
+
|
|
54
|
+
if (hasChanged && this.engine) {
|
|
55
|
+
// set advanced filters needs access to the latest coveoAdvancedQueryConfig
|
|
56
|
+
this.dispatchOnLoading(true);
|
|
57
|
+
this.setAdvancedFilters(this.engine);
|
|
58
|
+
this.submitSearch(true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@api
|
|
63
|
+
public fetchMoreResults() {
|
|
64
|
+
if (this.resultList?.state?.moreResultsAvailable) {
|
|
65
|
+
this.fetchingMoreResults = true;
|
|
66
|
+
this.resultList.fetchMoreResults();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@api
|
|
71
|
+
setInputValue(searchTerm: string) {
|
|
72
|
+
this.value = searchTerm || "";
|
|
73
|
+
this.handleValueChange(true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private _coveoAdvancedQueryConfig!: { [key: string]: any };
|
|
77
|
+
private dropdownRequestedOpen: boolean = false;
|
|
78
|
+
private recentSearches: Option[] = [];
|
|
79
|
+
private unsubscribeFromResultList: Unsubscribe = () => {};
|
|
80
|
+
private resultList: ResultList | null = null;
|
|
81
|
+
private resultsPerPage: ResultsPerPage | null = null;
|
|
82
|
+
private preloadedState?: SearchAppState;
|
|
83
|
+
private searchBox: SearchBox | null = null;
|
|
84
|
+
private value?: string = "";
|
|
85
|
+
private engine: SearchEngine | null = null;
|
|
86
|
+
private fetchingMoreResults: boolean = false;
|
|
87
|
+
private didRender = false;
|
|
88
|
+
|
|
89
|
+
private get hasCorrectCoveoConfig(): boolean {
|
|
90
|
+
const coveoConfigurations = [
|
|
91
|
+
this.coveoOrganizationId,
|
|
92
|
+
this.coveoPublicAccessToken,
|
|
93
|
+
this.coveoSearchHub,
|
|
94
|
+
this.coveoAdvancedQueryConfig
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return coveoConfigurations.every(
|
|
98
|
+
(val) =>
|
|
99
|
+
!!val && (typeof val === "string" || typeof val === "object")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private get isDropdownOpen() {
|
|
104
|
+
return (
|
|
105
|
+
this.dropdownRequestedOpen &&
|
|
106
|
+
this.recentSearches &&
|
|
107
|
+
this.recentSearches.length > 0 &&
|
|
108
|
+
!this.value
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
constructor() {
|
|
113
|
+
super();
|
|
114
|
+
|
|
115
|
+
this.updateRecentSearches();
|
|
116
|
+
this.value = getSidebarSearchParams() || "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
disconnectedCallback() {
|
|
120
|
+
this.unsubscribeFromResultList();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
renderedCallback() {
|
|
124
|
+
// in prod some of the critical coveo configuration attributes seem to arrive later
|
|
125
|
+
// so I needed to put this coveo startup flow here where we can wait for them to arrive
|
|
126
|
+
if (this.hasCorrectCoveoConfig && !this.engine) {
|
|
127
|
+
this.initializeUserSession();
|
|
128
|
+
this.initializeCoveo();
|
|
129
|
+
} else if (!this.hasCorrectCoveoConfig) {
|
|
130
|
+
console.error(
|
|
131
|
+
"Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
|
|
132
|
+
{
|
|
133
|
+
coveoOrganizationId: this.coveoOrganizationId,
|
|
134
|
+
coveoPublicAccessToken: this.coveoPublicAccessToken,
|
|
135
|
+
coveoSearchHub: this.coveoSearchHub,
|
|
136
|
+
coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If this is the first render and there is a search value, trigger
|
|
142
|
+
// term highlighting
|
|
143
|
+
if (!this.didRender && this.value) {
|
|
144
|
+
this.dispatchHighlightedTermChange();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.didRender = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private initializeUserSession() {
|
|
151
|
+
const stringified = window.sessionStorage.getItem(SESSION_KEY);
|
|
152
|
+
if (!stringified) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const json = JSON.parse(stringified);
|
|
158
|
+
const hasSameConfig = Object.entries(
|
|
159
|
+
json.coveoAdvancedQueryConfig
|
|
160
|
+
).every(
|
|
161
|
+
([key, value]: [string, any]) =>
|
|
162
|
+
this.coveoAdvancedQueryConfig[key] === value
|
|
163
|
+
);
|
|
164
|
+
const hasSameQuery = json?.state?.query?.q === this.value;
|
|
165
|
+
if (hasSameConfig && hasSameQuery) {
|
|
166
|
+
this.preloadedState = json?.state;
|
|
167
|
+
} else {
|
|
168
|
+
window.sessionStorage.removeItem(SESSION_KEY);
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
console.error(e);
|
|
172
|
+
window.sessionStorage.removeItem(SESSION_KEY);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private initializeCoveo = () => {
|
|
177
|
+
try {
|
|
178
|
+
this.engine = buildSearchEngine({
|
|
179
|
+
organizationId: this.coveoOrganizationId,
|
|
180
|
+
publicAccessToken: this.coveoPublicAccessToken,
|
|
181
|
+
searchHub: this.coveoSearchHub,
|
|
182
|
+
preloadedState: this.preloadedState
|
|
183
|
+
});
|
|
184
|
+
} catch (ex) {
|
|
185
|
+
console.error(`Coveo engine failed to initialize (${ex})`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!this.engine) {
|
|
189
|
+
console.error("Coveo engine failed to initialize!");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (this.preloadedState) {
|
|
194
|
+
const actions = loadQueryActions(this.engine);
|
|
195
|
+
this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.resultList = buildResultList(this.engine, {
|
|
199
|
+
options: { fieldsToInclude: ["sfhtml_url__c"] }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.resultsPerPage = buildResultsPerPage(this.engine, {
|
|
203
|
+
initialState: {
|
|
204
|
+
numberOfResults: DEFAULT_RESULT_COUNT
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.searchBox = buildSearchBox(this.engine, {
|
|
209
|
+
options: { numberOfSuggestions: 3 }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this.setAdvancedFilters(this.engine);
|
|
213
|
+
|
|
214
|
+
this.unsubscribeFromResultList = this.resultList?.subscribe(
|
|
215
|
+
this.onResultListChange
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (this.value) {
|
|
219
|
+
this.dispatchHighlightedTermChange();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!this.preloadedState) {
|
|
223
|
+
if (this.value) {
|
|
224
|
+
this.submitSearch();
|
|
225
|
+
} else {
|
|
226
|
+
this.dispatchOnLoading(false);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
private onResultListChange = () => {
|
|
232
|
+
if (!this.resultList) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { isLoading, firstSearchExecuted, results } =
|
|
237
|
+
this.resultList.state;
|
|
238
|
+
|
|
239
|
+
if ((!firstSearchExecuted && !isLoading) || !this.value) {
|
|
240
|
+
// coveo search is firing off some unwanted payloads on startup
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!isLoading) {
|
|
245
|
+
this.dispatchOnChangeEvent(results);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!this.fetchingMoreResults) {
|
|
249
|
+
this.dispatchOnLoading(isLoading);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!isLoading && this.fetchingMoreResults) {
|
|
253
|
+
this.fetchingMoreResults = false;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
private setAdvancedFilters(engine: SearchEngine) {
|
|
258
|
+
const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
|
|
259
|
+
(result, [key, value]) =>
|
|
260
|
+
`${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
|
|
261
|
+
value
|
|
262
|
+
)}")`,
|
|
263
|
+
""
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const registerSearchQueryAction = loadAdvancedSearchQueryActions(
|
|
267
|
+
engine
|
|
268
|
+
).registerAdvancedSearchQueries({
|
|
269
|
+
aq: aq
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
engine.dispatch(registerSearchQueryAction);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private normalizeCoveoResult = ({
|
|
276
|
+
clickUri,
|
|
277
|
+
excerpt,
|
|
278
|
+
excerptHighlights,
|
|
279
|
+
raw: { sfhtml_url__c },
|
|
280
|
+
title,
|
|
281
|
+
titleHighlights,
|
|
282
|
+
uniqueId
|
|
283
|
+
}: Result): SidebarSearchResult => {
|
|
284
|
+
// discussion about this normalization here: https://salesforce-internal.slack.com/archives/C020S6784JX/p1639506238376600
|
|
285
|
+
const extension =
|
|
286
|
+
this.coveoAdvancedQueryConfig.path_segment_4 === "references"
|
|
287
|
+
? ""
|
|
288
|
+
: ".html";
|
|
289
|
+
const pathname = sfhtml_url__c
|
|
290
|
+
? `/docs/${sfhtml_url__c}`
|
|
291
|
+
: `${new URL(clickUri).pathname}${extension}`;
|
|
292
|
+
const href = `${pathname}?q=${this.value}`;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
title,
|
|
296
|
+
titleHighlights,
|
|
297
|
+
excerpt,
|
|
298
|
+
excerptHighlights,
|
|
299
|
+
uniqueId,
|
|
300
|
+
href,
|
|
301
|
+
selected: pathname === window.location.pathname
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
private updateSessionStorage() {
|
|
306
|
+
window.sessionStorage.setItem(
|
|
307
|
+
SESSION_KEY,
|
|
308
|
+
JSON.stringify({
|
|
309
|
+
coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
|
|
310
|
+
state: this.engine?.state
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private dispatchOnChangeEvent(results: Result[]) {
|
|
316
|
+
this.updateSessionStorage();
|
|
317
|
+
this.dispatchEvent(
|
|
318
|
+
new CustomEvent("change", {
|
|
319
|
+
detail: {
|
|
320
|
+
results: results.map(this.normalizeCoveoResult),
|
|
321
|
+
value: this.value
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private dispatchOnLoading(loading: boolean) {
|
|
328
|
+
this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private dispatchHighlightedTermChange() {
|
|
332
|
+
this.dispatchEvent(
|
|
333
|
+
new CustomEvent("highlightedtermchange", {
|
|
334
|
+
detail: this.value,
|
|
335
|
+
composed: true,
|
|
336
|
+
bubbles: true
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private updateRecentSearches = () =>
|
|
342
|
+
(this.recentSearches = UserRecentSearches.get().map((item: string) => ({
|
|
343
|
+
id: item,
|
|
344
|
+
label: item
|
|
345
|
+
})));
|
|
346
|
+
|
|
347
|
+
private dispatchSidebarSearchChange(searchTerm: string | undefined): void {
|
|
348
|
+
this.dispatchEvent(
|
|
349
|
+
new CustomEvent("sidebarsearchchange", {
|
|
350
|
+
detail: searchTerm ?? "",
|
|
351
|
+
bubbles: true,
|
|
352
|
+
composed: true
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private submitSearch = debounce((isSyncingSearchValue = false) => {
|
|
358
|
+
if (this.searchBox) {
|
|
359
|
+
UserRecentSearches.add(this.value);
|
|
360
|
+
this.updateRecentSearches();
|
|
361
|
+
|
|
362
|
+
// When `isSyncingSearchValue` is true, we are submitting in a case
|
|
363
|
+
// where the search box is already synced correctly.
|
|
364
|
+
if (!isSyncingSearchValue) {
|
|
365
|
+
this.dispatchSidebarSearchChange(this.value);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.searchBox.updateText(this.value || "");
|
|
369
|
+
this.searchBox.submit();
|
|
370
|
+
}
|
|
371
|
+
}, SEARCH_DEBOUNCE_DELAY);
|
|
372
|
+
|
|
373
|
+
private onClickRecentSearch(e: CustomEvent) {
|
|
374
|
+
this.value = e.detail;
|
|
375
|
+
this.dispatchSidebarSearchChange(this.value);
|
|
376
|
+
this.submitSearch(true);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private onDropdownRequestClose(e: CustomEvent) {
|
|
380
|
+
switch (e.detail as PopoverRequestCloseType) {
|
|
381
|
+
case "interact-outside":
|
|
382
|
+
case "keypress-escape":
|
|
383
|
+
this.dropdownRequestedOpen = false;
|
|
384
|
+
break;
|
|
385
|
+
default:
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private onInputChange(e: CustomEvent) {
|
|
391
|
+
this.value = e.detail;
|
|
392
|
+
|
|
393
|
+
this.handleValueChange(false);
|
|
394
|
+
this.dispatchHighlightedTermChange();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private onInputFocus() {
|
|
398
|
+
this.dropdownRequestedOpen = true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private onInputClear() {
|
|
402
|
+
this.dropdownRequestedOpen = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private handleValueChange(isSyncingSearchValue = false) {
|
|
406
|
+
if (this.value) {
|
|
407
|
+
this.dispatchOnLoading(true);
|
|
408
|
+
if (this.searchBox) {
|
|
409
|
+
this.submitSearch(isSyncingSearchValue);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
// coveo's reaction to an empty value triggers a return of results
|
|
413
|
+
// bootlegging our own ux here`
|
|
414
|
+
this.dispatchOnLoading(false);
|
|
415
|
+
this.dispatchOnChangeEvent([]);
|
|
416
|
+
// Empty search values are not submitted for search, so we need to manually
|
|
417
|
+
// trigger a search change on our own here
|
|
418
|
+
this.dispatchSidebarSearchChange(this.value);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@import "helpers/reset";
|
|
2
|
+
@import "helpers/text";
|
|
3
|
+
@import "helpers/sidebar";
|
|
4
|
+
|
|
5
|
+
a {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sidebar-item {
|
|
11
|
+
padding: var(--dx-g-spacing-smd) var(--dx-g-spacing-md);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.search-text {
|
|
15
|
+
font-size: var(--dx-g-text-xs);
|
|
16
|
+
font-family: var(--dx-g-font-sans);
|
|
17
|
+
line-height: 18px;
|
|
18
|
+
overflow-wrap: break-word;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.description {
|
|
22
|
+
-webkit-line-clamp: 3;
|
|
23
|
+
overflow-wrap: break-word;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.title {
|
|
27
|
+
color: var(--dx-g-text-heading-color);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bold {
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
a > *:not(:last-child) {
|
|
35
|
+
margin-bottom: 2px;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a class={className} href={href}>
|
|
3
|
+
<span class="title search-text">
|
|
4
|
+
<template for:each={titleChunks} for:item="chunk">
|
|
5
|
+
<span class={chunk.class} key={chunk.id}>{chunk.value}</span>
|
|
6
|
+
</template>
|
|
7
|
+
</span>
|
|
8
|
+
<span class="description dx-text-body-3 sidebar-item-truncate-text">
|
|
9
|
+
<template for:each={descriptionChunks} for:item="chunk">
|
|
10
|
+
<span class={chunk.class} key={chunk.id}>{chunk.value}</span>
|
|
11
|
+
</template>
|
|
12
|
+
</span>
|
|
13
|
+
</a>
|
|
14
|
+
</template>
|