@salesforcedevs/dx-components 0.10.1-alpha.13 → 0.10.1-alpha.130
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 +3 -1
- package/package.json +2 -2
- package/src/assets/svg/docs-empty-state-small.svg +72 -0
- package/src/assets/svg/sidebar-loading.svg +34 -0
- package/src/modules/dx/cardGrid/__tests__/cardMockProps.ts +78 -148
- package/src/modules/dx/codeBlock/codeBlock.stories.ts +2 -69
- package/src/modules/dx/codeBlock/codeBlock.ts +5 -1
- package/src/modules/dx/codeBlock/darkTheme.css +0 -1
- package/src/modules/dx/codeBlock/lightTheme.css +0 -1
- 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/footer/links.ts +3 -5
- 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/lazy/lazy.ts +0 -7
- package/src/modules/dx/newsletterForm/newsletterForm.html +5 -3
- package/src/modules/dx/searchResults/coveo.css +314 -111
- package/src/modules/dx/sidebar/__tests__/mockDocContent.ts +48 -0
- package/src/modules/dx/sidebar/__tests__/mockPageReference.ts +8 -0
- package/src/modules/dx/sidebar/sidebar.css +38 -17
- package/src/modules/dx/sidebar/sidebar.html +62 -58
- package/src/modules/dx/sidebar/sidebar.stories.ts +53 -54
- package/src/modules/dx/sidebar/sidebar.ts +99 -10
- package/src/modules/dx/sidebarSearch/__tests__/mockPreloadedState.ts +8843 -0
- package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +121 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +281 -120
- package/src/modules/dx/sidebarSearch/utils.ts +2 -0
- package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +12 -2
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +1 -1
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +8 -2
- package/src/modules/dx/spinner/__tests__/spinner.test.ts +37 -0
- package/src/modules/dx/spinner/spinner.css +750 -0
- package/src/modules/dx/spinner/spinner.html +6 -0
- package/src/modules/dx/spinner/spinner.stories.ts +69 -0
- package/src/modules/dx/spinner/spinner.ts +20 -0
- 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/helpers/table/table.css +23 -7
- package/src/modules/utils/coveo/coveo.ts +50 -12
- package/src/modules/utils/highlight/__tests__/highlight.test.ts +154 -0
- package/src/modules/utils/highlight/highlight.ts +32 -0
- package/src/modules/utils/jest/jest.ts +35 -0
- 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 +12 -17
- package/src/modules/utils/regexps/regexps.ts +5 -0
- package/src/modules/utils/tests/tests.ts +18 -11
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
import mockPreloadedState from "./mockPreloadedState";
|
|
7
|
+
import { SESSION_KEY } from "../sidebarSearch";
|
|
8
|
+
|
|
9
|
+
const TAG = "dx-sidebar-search";
|
|
10
|
+
const render = createRenderComponent(TAG, SidebarSearch);
|
|
11
|
+
|
|
12
|
+
const PROPS = coveoXMLDocsConfig;
|
|
13
|
+
const QUERY = "test";
|
|
14
|
+
|
|
15
|
+
describe(TAG, () => {
|
|
16
|
+
afterEach(unmockQueryParameter);
|
|
17
|
+
|
|
18
|
+
it("renders with the correct initial empty query", () => {
|
|
19
|
+
const element = render(PROPS);
|
|
20
|
+
|
|
21
|
+
const input = element.shadowRoot.querySelector("dx-input");
|
|
22
|
+
|
|
23
|
+
expect(input.value).toEqual("");
|
|
24
|
+
|
|
25
|
+
return expect(element).toBeAccessible();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders with the correct initial query", () => {
|
|
29
|
+
mockQueryParameter(QUERY);
|
|
30
|
+
|
|
31
|
+
const element = render(PROPS);
|
|
32
|
+
|
|
33
|
+
const input = element.shadowRoot.querySelector("dx-input");
|
|
34
|
+
|
|
35
|
+
expect(input.value).toEqual(QUERY);
|
|
36
|
+
|
|
37
|
+
return expect(element).toBeAccessible();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("correctly consumes a valid session storage object", () => {
|
|
41
|
+
mockQueryParameter(mockPreloadedState.query.q);
|
|
42
|
+
|
|
43
|
+
window.sessionStorage.setItem(
|
|
44
|
+
SESSION_KEY,
|
|
45
|
+
JSON.stringify(mockPreloadedState)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const mockOnChange = jest.fn();
|
|
49
|
+
|
|
50
|
+
const element = render(PROPS);
|
|
51
|
+
element.addEventListener("change", mockOnChange);
|
|
52
|
+
|
|
53
|
+
return Promise.resolve().then(() => {
|
|
54
|
+
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
|
55
|
+
|
|
56
|
+
expect(element).toBeAccessible();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("highlighting", () => {
|
|
61
|
+
afterEach(unmockQueryParameter);
|
|
62
|
+
|
|
63
|
+
it("triggers on initial render when a search value exists", () => {
|
|
64
|
+
mockQueryParameter(QUERY);
|
|
65
|
+
const element = createElement(TAG, { is: SidebarSearch });
|
|
66
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
67
|
+
renderLightningElement(element, PROPS);
|
|
68
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
type: "highlightedtermchange",
|
|
71
|
+
detail: QUERY
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not trigger on initial render when a search value does not exist", () => {
|
|
77
|
+
const element = createElement(TAG, { is: SidebarSearch });
|
|
78
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
79
|
+
renderLightningElement(element, PROPS);
|
|
80
|
+
expect(dispatchEventSpy).not.toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({ type: "highlightedtermchange" })
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("triggers on input change with non-empty value", () => {
|
|
86
|
+
const element = render(PROPS);
|
|
87
|
+
const dxInput = element.shadowRoot.querySelector("dx-input");
|
|
88
|
+
const inputEl = dxInput.shadowRoot.querySelector("input");
|
|
89
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
90
|
+
|
|
91
|
+
inputEl.focus();
|
|
92
|
+
inputEl.value = QUERY;
|
|
93
|
+
inputEl.dispatchEvent(new KeyboardEvent("input"));
|
|
94
|
+
|
|
95
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
type: "highlightedtermchange",
|
|
98
|
+
detail: QUERY
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("triggers on input change with empty value", () => {
|
|
104
|
+
const element = render(PROPS);
|
|
105
|
+
const dxInput = element.shadowRoot.querySelector("dx-input");
|
|
106
|
+
const inputEl = dxInput.shadowRoot.querySelector("input");
|
|
107
|
+
const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
|
|
108
|
+
|
|
109
|
+
inputEl.focus();
|
|
110
|
+
inputEl.value = "";
|
|
111
|
+
inputEl.dispatchEvent(new KeyboardEvent("input"));
|
|
112
|
+
|
|
113
|
+
expect(dispatchEventSpy).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
type: "highlightedtermchange",
|
|
116
|
+
detail: ""
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -1,207 +1,363 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
2
|
import debounce from "debounce";
|
|
3
|
-
import { buildSearchEngine } from "utils/coveo";
|
|
4
|
-
import { RecentSearches } from "utils/recentSearches";
|
|
5
3
|
import {
|
|
6
4
|
Result,
|
|
7
5
|
ResultList,
|
|
6
|
+
ResultsPerPage,
|
|
8
7
|
SearchBox,
|
|
9
|
-
SearchBoxOptions,
|
|
10
8
|
buildResultList,
|
|
9
|
+
buildResultsPerPage,
|
|
11
10
|
buildSearchBox,
|
|
12
11
|
loadAdvancedSearchQueryActions,
|
|
13
|
-
SearchEngine
|
|
12
|
+
SearchEngine,
|
|
13
|
+
SearchAppState,
|
|
14
|
+
loadQueryActions
|
|
14
15
|
} from "@coveo/headless";
|
|
16
|
+
import { buildSearchEngine, getSidebarSearchParams } from "utils/coveo";
|
|
17
|
+
import { RecentSearches } from "utils/recentSearches";
|
|
18
|
+
import { toJson } from "utils/normalizers";
|
|
15
19
|
import { Option, PopoverRequestCloseType } from "typings/custom";
|
|
16
20
|
|
|
21
|
+
export const SESSION_KEY = "dx-sidebar-search-state";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_RESULT_COUNT = 20;
|
|
24
|
+
const SEARCH_DEBOUNCE_DELAY = 780;
|
|
25
|
+
|
|
17
26
|
const UserRecentSearches = new RecentSearches();
|
|
18
27
|
|
|
19
|
-
const
|
|
28
|
+
const normalizeCoveoAdvancedQueryValue = (version: string): string =>
|
|
20
29
|
version.split(".").join("\\.");
|
|
21
30
|
|
|
22
|
-
const normalizeCoveoResult = ({
|
|
23
|
-
title,
|
|
24
|
-
titleHighlights,
|
|
25
|
-
excerpt,
|
|
26
|
-
excerptHighlights,
|
|
27
|
-
uniqueId,
|
|
28
|
-
raw: { sfhtml_url__c }
|
|
29
|
-
}: Result) => ({
|
|
30
|
-
title,
|
|
31
|
-
titleHighlights,
|
|
32
|
-
excerpt,
|
|
33
|
-
excerptHighlights,
|
|
34
|
-
uniqueId,
|
|
35
|
-
href: `/docs/${sfhtml_url__c}`
|
|
36
|
-
});
|
|
37
|
-
|
|
38
31
|
export default class SidebarSearch extends LightningElement {
|
|
39
32
|
@api coveoOrganizationId!: string;
|
|
40
33
|
@api coveoPublicAccessToken!: string;
|
|
41
34
|
@api coveoSearchHub!: string;
|
|
42
|
-
@api
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
@api
|
|
36
|
+
get coveoAdvancedQueryConfig(): { [key: string]: any } {
|
|
37
|
+
return this._coveoAdvancedQueryConfig;
|
|
38
|
+
}
|
|
39
|
+
set coveoAdvancedQueryConfig(value) {
|
|
40
|
+
this._coveoAdvancedQueryConfig = toJson(value);
|
|
41
|
+
}
|
|
46
42
|
|
|
43
|
+
@api
|
|
44
|
+
public fetchMoreResults() {
|
|
45
|
+
if (this.resultList?.state?.moreResultsAvailable) {
|
|
46
|
+
this.fetchingMoreResults = true;
|
|
47
|
+
this.resultList.fetchMoreResults();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@api
|
|
52
|
+
setInputValue(searchTerm: string) {
|
|
53
|
+
this.value = searchTerm || "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _coveoAdvancedQueryConfig!: object;
|
|
47
57
|
private dropdownRequestedOpen: boolean = false;
|
|
48
58
|
private recentSearches: Option[] = [];
|
|
49
59
|
private resultList: ResultList | null = null;
|
|
60
|
+
private resultsPerPage: ResultsPerPage | null = null;
|
|
61
|
+
private preloadedState?: SearchAppState;
|
|
50
62
|
private searchBox: SearchBox | null = null;
|
|
51
|
-
private value?: string =
|
|
63
|
+
private value?: string = "";
|
|
52
64
|
private engine: SearchEngine | null = null;
|
|
65
|
+
private fetchingMoreResults: boolean = false;
|
|
66
|
+
private didRender = false;
|
|
67
|
+
|
|
68
|
+
private get hasCorrectCoveoConfig(): boolean {
|
|
69
|
+
const coveoConfigurations = [
|
|
70
|
+
this.coveoOrganizationId,
|
|
71
|
+
this.coveoPublicAccessToken,
|
|
72
|
+
this.coveoSearchHub,
|
|
73
|
+
this.coveoAdvancedQueryConfig
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
return coveoConfigurations.every(
|
|
77
|
+
(val) =>
|
|
78
|
+
!!val && (typeof val === "string" || typeof val === "object")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
53
81
|
|
|
54
82
|
private get isDropdownOpen() {
|
|
55
83
|
return (
|
|
56
84
|
this.dropdownRequestedOpen &&
|
|
57
85
|
this.recentSearches &&
|
|
58
86
|
this.recentSearches.length > 0 &&
|
|
59
|
-
this.
|
|
87
|
+
!this.value
|
|
60
88
|
);
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
91
|
+
constructor() {
|
|
92
|
+
super();
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
this.
|
|
94
|
+
this.updateRecentSearches();
|
|
95
|
+
this.value = getSidebarSearchParams() || "";
|
|
69
96
|
}
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
const coveoConfigurations = [
|
|
73
|
-
this.coveoOrganizationId,
|
|
74
|
-
this.coveoPublicAccessToken,
|
|
75
|
-
this.coveoSearchHub,
|
|
76
|
-
this.coveoObjectType,
|
|
77
|
-
this.coveoLanguage,
|
|
78
|
-
this.coveoRelease,
|
|
79
|
-
this.coveoDeliverable
|
|
80
|
-
];
|
|
98
|
+
queryConfigValue: string | null = null;
|
|
81
99
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
} else {
|
|
100
|
+
renderedCallback() {
|
|
101
|
+
// in prod some of the critical coveo configuration attributes seem to arrive later
|
|
102
|
+
// so I needed to put this coveo startup flow here where we can wait for them to arrive
|
|
103
|
+
if (this.hasCorrectCoveoConfig && !this.engine) {
|
|
104
|
+
console.log("coveo config is good");
|
|
105
|
+
this.initializeUserSession();
|
|
106
|
+
this.initializeCoveo();
|
|
107
|
+
} else if (!this.hasCorrectCoveoConfig) {
|
|
90
108
|
console.error(
|
|
91
|
-
"Incorrect coveo configuration provided. All coveo configuration attributes must be defined
|
|
109
|
+
"Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
|
|
92
110
|
{
|
|
93
111
|
coveoOrganizationId: this.coveoOrganizationId,
|
|
94
112
|
coveoPublicAccessToken: this.coveoPublicAccessToken,
|
|
95
113
|
coveoSearchHub: this.coveoSearchHub,
|
|
96
|
-
|
|
97
|
-
coveoLanguage: this.coveoLanguage,
|
|
98
|
-
coveoRelease: this.coveoRelease,
|
|
99
|
-
coveoDeliverable: this.coveoDeliverable
|
|
114
|
+
coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
|
|
100
115
|
}
|
|
101
116
|
);
|
|
102
117
|
}
|
|
118
|
+
|
|
119
|
+
const queryConfigValue = JSON.stringify(this.coveoAdvancedQueryConfig);
|
|
120
|
+
console.log('query config value', queryConfigValue);
|
|
121
|
+
if (this.queryConfigValue !== queryConfigValue) {
|
|
122
|
+
if (this.queryConfigValue && this.engine) {
|
|
123
|
+
console.log('setting query config again')
|
|
124
|
+
this.setAdvancedFilters(this.engine);
|
|
125
|
+
}
|
|
126
|
+
this.queryConfigValue = queryConfigValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If this is the first render and there is a search value, trigger
|
|
130
|
+
// term highlighting
|
|
131
|
+
if (!this.didRender && this.value) {
|
|
132
|
+
this.dispatchHighlightedTermChange();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.didRender = true;
|
|
103
136
|
}
|
|
104
137
|
|
|
105
|
-
private
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
private initializeUserSession() {
|
|
139
|
+
const stringified = window.sessionStorage.getItem(SESSION_KEY);
|
|
140
|
+
if (!stringified) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
109
143
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
try {
|
|
145
|
+
const json = JSON.parse(stringified);
|
|
146
|
+
const hasSameConfig = Object.entries(
|
|
147
|
+
json.coveoAdvancedQueryConfig
|
|
148
|
+
).every(
|
|
149
|
+
([key, value]: [string, any]) =>
|
|
150
|
+
this.coveoAdvancedQueryConfig[key] === value
|
|
151
|
+
);
|
|
152
|
+
const hasSameQuery = json?.state?.query?.q === this.value;
|
|
153
|
+
if (hasSameConfig && hasSameQuery) {
|
|
154
|
+
console.log(
|
|
155
|
+
"valid session storage found for value =",
|
|
156
|
+
this.value
|
|
157
|
+
);
|
|
158
|
+
this.preloadedState = json?.state;
|
|
159
|
+
} else {
|
|
160
|
+
window.sessionStorage.removeItem(SESSION_KEY);
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error(e);
|
|
164
|
+
window.sessionStorage.removeItem(SESSION_KEY);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
115
167
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
private initializeCoveo = () => {
|
|
169
|
+
try {
|
|
170
|
+
this.engine = buildSearchEngine({
|
|
171
|
+
organizationId: this.coveoOrganizationId,
|
|
172
|
+
publicAccessToken: this.coveoPublicAccessToken,
|
|
173
|
+
searchHub: this.coveoSearchHub,
|
|
174
|
+
preloadedState: this.preloadedState
|
|
119
175
|
});
|
|
120
|
-
|
|
176
|
+
} catch (ex) {
|
|
177
|
+
console.error(`Coveo engine failed to initialize (${ex})`);
|
|
121
178
|
}
|
|
122
179
|
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
if (!this.engine) {
|
|
181
|
+
console.error("Coveo engine failed to initialize!");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
127
184
|
|
|
128
|
-
|
|
185
|
+
if (this.preloadedState) {
|
|
186
|
+
const actions = loadQueryActions(this.engine);
|
|
187
|
+
this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.resultList = buildResultList(this.engine, {
|
|
191
|
+
options: { fieldsToInclude: ["sfhtml_url__c"] }
|
|
192
|
+
});
|
|
129
193
|
|
|
130
|
-
|
|
131
|
-
|
|
194
|
+
this.resultsPerPage = buildResultsPerPage(this.engine, {
|
|
195
|
+
initialState: {
|
|
196
|
+
numberOfResults: DEFAULT_RESULT_COUNT
|
|
132
197
|
}
|
|
198
|
+
});
|
|
133
199
|
|
|
134
|
-
|
|
200
|
+
this.searchBox = buildSearchBox(this.engine, {
|
|
201
|
+
options: { numberOfSuggestions: 3 }
|
|
135
202
|
});
|
|
136
|
-
}
|
|
137
203
|
|
|
138
|
-
|
|
139
|
-
|
|
204
|
+
this.setAdvancedFilters(this.engine);
|
|
205
|
+
|
|
206
|
+
this.resultList?.subscribe(this.onResultListChange);
|
|
207
|
+
|
|
208
|
+
if (this.value) {
|
|
209
|
+
this.dispatchHighlightedTermChange();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!this.preloadedState) {
|
|
213
|
+
if (this.value) {
|
|
214
|
+
this.submitSearch();
|
|
215
|
+
} else {
|
|
216
|
+
this.dispatchOnLoading(false);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
private onResultListChange = () => {
|
|
222
|
+
if (!this.resultList) {
|
|
140
223
|
return;
|
|
141
224
|
}
|
|
142
225
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
226
|
+
const { isLoading, firstSearchExecuted, results } =
|
|
227
|
+
this.resultList.state;
|
|
228
|
+
|
|
229
|
+
if ((!firstSearchExecuted && !isLoading) || !this.value) {
|
|
230
|
+
// coveo search is firing off some unwanted payloads on startup
|
|
231
|
+
console.log(
|
|
232
|
+
"returning in results list change",
|
|
233
|
+
this.value,
|
|
234
|
+
!firstSearchExecuted && !isLoading
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isLoading) {
|
|
240
|
+
console.log("dispatching on change event");
|
|
241
|
+
this.dispatchOnChangeEvent(results);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!this.fetchingMoreResults) {
|
|
245
|
+
this.dispatchOnLoading(isLoading);
|
|
246
|
+
}
|
|
149
247
|
|
|
150
|
-
|
|
151
|
-
|
|
248
|
+
if (!isLoading && this.fetchingMoreResults) {
|
|
249
|
+
this.fetchingMoreResults = false;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private setAdvancedFilters(engine: SearchEngine) {
|
|
254
|
+
const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
|
|
255
|
+
(result, [key, value]) =>
|
|
256
|
+
`${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
|
|
257
|
+
value
|
|
258
|
+
)}")`,
|
|
152
259
|
""
|
|
153
260
|
);
|
|
154
|
-
const advancedSearchQueryActions = loadAdvancedSearchQueryActions(
|
|
155
|
-
this.engine
|
|
156
|
-
);
|
|
157
261
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
262
|
+
const registerSearchQueryAction = loadAdvancedSearchQueryActions(
|
|
263
|
+
engine
|
|
264
|
+
).registerAdvancedSearchQueries({
|
|
265
|
+
aq: aq
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
engine.dispatch(registerSearchQueryAction);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private normalizeCoveoResult = ({
|
|
272
|
+
clickUri,
|
|
273
|
+
excerpt,
|
|
274
|
+
excerptHighlights,
|
|
275
|
+
raw: { sfhtml_url__c },
|
|
276
|
+
title,
|
|
277
|
+
titleHighlights,
|
|
278
|
+
uniqueId
|
|
279
|
+
}: Result) => {
|
|
280
|
+
const pathname = sfhtml_url__c
|
|
281
|
+
? `/docs/${sfhtml_url__c}`
|
|
282
|
+
: `${new URL(clickUri).pathname}.html`;
|
|
283
|
+
const href = `${pathname}?q=${this.value}`;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
title,
|
|
287
|
+
titleHighlights,
|
|
288
|
+
excerpt,
|
|
289
|
+
excerptHighlights,
|
|
290
|
+
uniqueId,
|
|
291
|
+
href,
|
|
292
|
+
selected: pathname === window.location.pathname
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
private updateSessionStorage() {
|
|
297
|
+
console.log("updating session storage");
|
|
298
|
+
window.sessionStorage.setItem(
|
|
299
|
+
SESSION_KEY,
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
|
|
302
|
+
state: this.engine?.state
|
|
161
303
|
})
|
|
162
304
|
);
|
|
163
305
|
}
|
|
164
306
|
|
|
165
307
|
private dispatchOnChangeEvent(results: Result[]) {
|
|
308
|
+
this.updateSessionStorage();
|
|
166
309
|
this.dispatchEvent(
|
|
167
310
|
new CustomEvent("change", {
|
|
168
311
|
detail: {
|
|
169
|
-
results: results.map(normalizeCoveoResult),
|
|
312
|
+
results: results.map(this.normalizeCoveoResult),
|
|
170
313
|
value: this.value
|
|
171
314
|
}
|
|
172
315
|
})
|
|
173
316
|
);
|
|
174
317
|
}
|
|
175
318
|
|
|
176
|
-
private dispatchOnLoading(
|
|
319
|
+
private dispatchOnLoading(loading: boolean) {
|
|
320
|
+
console.log("dispatching onloading", loading);
|
|
321
|
+
this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private dispatchHighlightedTermChange() {
|
|
177
325
|
this.dispatchEvent(
|
|
178
|
-
new CustomEvent("
|
|
179
|
-
detail:
|
|
326
|
+
new CustomEvent("highlightedtermchange", {
|
|
327
|
+
detail: this.value,
|
|
328
|
+
composed: true,
|
|
329
|
+
bubbles: true
|
|
180
330
|
})
|
|
181
331
|
);
|
|
182
332
|
}
|
|
183
333
|
|
|
184
|
-
private
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
334
|
+
private updateRecentSearches = () =>
|
|
335
|
+
(this.recentSearches = UserRecentSearches.get().map(
|
|
336
|
+
(item: { id: string; label: string }) => ({
|
|
337
|
+
id: item,
|
|
338
|
+
label: item
|
|
339
|
+
})
|
|
340
|
+
));
|
|
192
341
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
342
|
+
private dispatchSidebarSearchChange(searchTerm: string | undefined): void {
|
|
343
|
+
this.dispatchEvent(
|
|
344
|
+
new CustomEvent("sidebarsearchchange", {
|
|
345
|
+
detail: searchTerm ?? "",
|
|
346
|
+
bubbles: true,
|
|
347
|
+
composed: true
|
|
348
|
+
})
|
|
349
|
+
);
|
|
197
350
|
}
|
|
198
351
|
|
|
199
352
|
private submitSearch = debounce(() => {
|
|
200
|
-
this.searchBox
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
353
|
+
if (this.searchBox) {
|
|
354
|
+
UserRecentSearches.add(this.value);
|
|
355
|
+
this.updateRecentSearches();
|
|
356
|
+
|
|
357
|
+
this.searchBox.updateText(this.value || "");
|
|
358
|
+
this.searchBox.submit();
|
|
359
|
+
}
|
|
360
|
+
}, SEARCH_DEBOUNCE_DELAY);
|
|
205
361
|
|
|
206
362
|
private onClickRecentSearch(e: CustomEvent) {
|
|
207
363
|
this.value = e.detail;
|
|
@@ -222,15 +378,20 @@ export default class SidebarSearch extends LightningElement {
|
|
|
222
378
|
private onInputChange(e: CustomEvent) {
|
|
223
379
|
this.value = e.detail;
|
|
224
380
|
|
|
225
|
-
if (this.
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
381
|
+
if (this.value) {
|
|
382
|
+
this.dispatchOnLoading(true);
|
|
383
|
+
if (this.searchBox) {
|
|
384
|
+
this.submitSearch();
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// coveo's reaction to an empty value triggers a return of results
|
|
388
|
+
// bootlegging our own ux here`
|
|
389
|
+
this.dispatchOnLoading(false);
|
|
230
390
|
this.dispatchOnChangeEvent([]);
|
|
231
391
|
}
|
|
232
392
|
|
|
233
|
-
this.
|
|
393
|
+
this.dispatchHighlightedTermChange();
|
|
394
|
+
this.dispatchSidebarSearchChange(this.value);
|
|
234
395
|
}
|
|
235
396
|
|
|
236
397
|
private onInputFocus() {
|
|
@@ -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
|
+
});
|