@salesforcedevs/dx-components 1.32.0-alpha.8 → 1.32.0-docs-content-type-1
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 +47 -47
- package/src/modules/dx/agentMiawUi/agentMiawUi.css +4 -0
- package/src/modules/dx/agentMiawUi/agentMiawUi.html +3 -0
- package/src/modules/dx/agentMiawUi/agentMiawUi.ts +253 -0
- package/src/modules/dx/button/button.css +5 -0
- package/src/modules/dx/codeBlock/codeBlock.css +7 -0
- package/src/modules/dx/dropdownOption/dropdownOption.css +8 -13
- package/src/modules/dx/footer/footer.ts +55 -32
- package/src/modules/dx/globalHeader/globalHeader.html +2 -2
- package/src/modules/dx/globalHeader/globalHeader.ts +40 -30
- package/src/modules/dx/searchHybrid/searchHybrid.css +165 -0
- package/src/modules/dx/searchHybrid/searchHybrid.html +136 -0
- package/src/modules/dx/searchHybrid/searchHybrid.ts +306 -0
- package/src/modules/dx/searchResults/searchResults.css +6 -105
- package/src/modules/dx/searchResults/searchResults.html +91 -87
- package/src/modules/dx/searchResults/searchResults.ts +477 -174
- package/src/modules/dx/sidebar/sidebar.css +0 -1
- package/src/modules/dx/sidebar/sidebar.html +5 -1
- package/src/modules/dx/sidebar/sidebar.ts +29 -44
- package/src/modules/dx/sidebarOld/sidebarOld.html +0 -1
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +343 -159
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +25 -43
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +4 -12
- package/src/modules/dx/tree/treeHandler.ts +2 -1
- package/src/modules/dx/treeTile/treeTile.css +4 -0
- package/src/modules/dx/treeTile/treeTile.html +10 -0
- package/src/modules/dx/treeTile/treeTile.ts +4 -0
- package/src/modules/dxUtils/async/async.ts +32 -0
- package/src/modules/dxUtils/searchLocalePrefs/searchLocalePrefs.ts +193 -0
- package/LICENSE +0 -12
- package/src/modules/dxUtils/data360Search/data360Search.ts +0 -168
|
@@ -2,55 +2,34 @@
|
|
|
2
2
|
@import "dxHelpers/text";
|
|
3
3
|
@import "dxHelpers/commonTreeItem";
|
|
4
4
|
|
|
5
|
-
/* List item states per spec: Default, Hover (#F7F7F7), Selected (#E6F2FF), Focus (blue outline), Focus+Selected */
|
|
6
5
|
a {
|
|
7
6
|
display: flex;
|
|
8
7
|
flex-direction: column;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
/* Match "Results" heading alignment. Less item padding so total height unchanged when box has more inner padding. */
|
|
12
10
|
.sidebar-item {
|
|
13
|
-
padding:
|
|
14
|
-
|
|
15
|
-
transition: background-color var(--dx-g-transition-hue-1x, 0.1s ease);
|
|
16
|
-
overflow: visible;
|
|
11
|
+
padding: var(--dx-g-spacing-smd) var(--dx-g-spacing-lg)
|
|
12
|
+
var(--dx-g-spacing-smd) var(--dx-g-global-header-padding-horizontal);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
margin
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
border-radius: var(--dx-g-spacing-
|
|
25
|
-
transition: background-color var(--dx-g-transition-hue-1x, 0.1s ease);
|
|
15
|
+
.sidebar-item:not(.sidebar-item-selected):focus-visible {
|
|
16
|
+
padding: var(--dx-g-spacing-smd) 0;
|
|
17
|
+
margin: 0 var(--dx-g-spacing-lg) 0
|
|
18
|
+
var(--dx-g-global-header-padding-horizontal);
|
|
19
|
+
outline: var(--dx-g-spacing-2xs) solid var(--dx-g-blue-vibrant-40);
|
|
20
|
+
border-radius: var(--dx-g-spacing-2xs);
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/* Selected: light blue background in box shape only */
|
|
34
|
-
.sidebar-item-selected {
|
|
35
|
-
box-shadow: none;
|
|
36
|
-
color: var(--dx-g-blue-vibrant-40) !important;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.sidebar-item-selected .search-result {
|
|
40
|
-
background: var(--dx-g-cloud-blue-vibrant-95);
|
|
41
|
-
color: inherit;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/* Focus: only one box – remove anchor outline (from commonTreeItem), draw single blue outline on .search-result only */
|
|
45
|
-
.sidebar-item:focus-visible {
|
|
46
|
-
outline: none !important;
|
|
47
|
-
}
|
|
23
|
+
.sidebar-item-selected:focus-visible {
|
|
24
|
+
padding: 0 var(--dx-g-spacing-lg) 0
|
|
25
|
+
var(--dx-g-global-header-padding-horizontal);
|
|
26
|
+
outline: none;
|
|
48
27
|
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
28
|
+
.search-result {
|
|
29
|
+
outline: var(--dx-g-spacing-2xs) solid var(--dx-g-blue-vibrant-40);
|
|
30
|
+
border-radius: var(--dx-g-spacing-2xs);
|
|
31
|
+
padding: var(--dx-g-spacing-smd) 0;
|
|
32
|
+
}
|
|
54
33
|
}
|
|
55
34
|
|
|
56
35
|
.search-text {
|
|
@@ -73,13 +52,16 @@ a {
|
|
|
73
52
|
|
|
74
53
|
.title {
|
|
75
54
|
color: var(--dx-g-text-heading-color);
|
|
76
|
-
line-height: 18px;
|
|
77
|
-
display: block;
|
|
78
|
-
margin-bottom: 4px;
|
|
79
55
|
}
|
|
80
56
|
|
|
81
|
-
|
|
57
|
+
.title:hover {
|
|
58
|
+
color: var(--dx-g-blue-vibrant-50);
|
|
59
|
+
}
|
|
60
|
+
|
|
82
61
|
.bold {
|
|
83
|
-
background-color: var(--dx-g-yellow-vibrant-90);
|
|
84
62
|
font-weight: 700;
|
|
85
63
|
}
|
|
64
|
+
|
|
65
|
+
a > *:not(:last-child) {
|
|
66
|
+
margin-bottom: var(--dx-g-spacing-xs);
|
|
67
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
2
|
import cx from "classnames";
|
|
3
|
-
import {
|
|
4
|
-
import { HighlightedSections } from "typings/custom";
|
|
3
|
+
import { CoveoHighlights } from "typings/custom";
|
|
5
4
|
|
|
6
|
-
const toChunks = (value: string, highlights:
|
|
5
|
+
const toChunks = (value: string, highlights: CoveoHighlights) => {
|
|
7
6
|
if (!highlights || highlights.length < 1) {
|
|
8
7
|
return [
|
|
9
8
|
{
|
|
@@ -52,11 +51,11 @@ const toChunks = (value: string, highlights: HighlightedSections) => {
|
|
|
52
51
|
|
|
53
52
|
export default class SidebarSearchResult extends LightningElement {
|
|
54
53
|
@api description!: string;
|
|
55
|
-
@api descriptionHighlights!:
|
|
54
|
+
@api descriptionHighlights!: CoveoHighlights;
|
|
56
55
|
@api href!: string;
|
|
57
56
|
@api selected!: boolean;
|
|
58
57
|
@api header!: string;
|
|
59
|
-
@api titleHighlights!:
|
|
58
|
+
@api titleHighlights!: CoveoHighlights;
|
|
60
59
|
@api select!: Function;
|
|
61
60
|
|
|
62
61
|
private get titleChunks() {
|
|
@@ -73,13 +72,6 @@ export default class SidebarSearchResult extends LightningElement {
|
|
|
73
72
|
|
|
74
73
|
private onClick(e: PointerEvent) {
|
|
75
74
|
e.preventDefault();
|
|
76
|
-
track(e.currentTarget!, "custEv_scopedSearchItemSelected", {
|
|
77
|
-
click_text: this.header,
|
|
78
|
-
click_url: this.href,
|
|
79
|
-
element_title: this.header,
|
|
80
|
-
element_type: "link",
|
|
81
|
-
content_category: "documentation"
|
|
82
|
-
});
|
|
83
75
|
this.select();
|
|
84
76
|
window.location.href = this.href;
|
|
85
77
|
}
|
|
@@ -64,7 +64,8 @@ export class TreeHandler {
|
|
|
64
64
|
isExpanded: node.isExpanded || false,
|
|
65
65
|
isChildrenLoading: node.isChildrenLoading || false,
|
|
66
66
|
link: node.link,
|
|
67
|
-
method: node.method
|
|
67
|
+
method: node.method,
|
|
68
|
+
showForwardArrow: node.showForwardArrow
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
const mapItem = { node: convertedNode, parent };
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
value={methodText}
|
|
38
38
|
color={metadataBadgeColor}
|
|
39
39
|
></dx-metadata-badge>
|
|
40
|
+
<div
|
|
41
|
+
class="flex tile-icon tile-forward-arrow"
|
|
42
|
+
if:true={showForwardArrow}
|
|
43
|
+
>
|
|
44
|
+
<dx-icon
|
|
45
|
+
sprite="utility"
|
|
46
|
+
symbol="forward"
|
|
47
|
+
size={iconSize}
|
|
48
|
+
></dx-icon>
|
|
49
|
+
</div>
|
|
40
50
|
</div>
|
|
41
51
|
</a>
|
|
42
52
|
</template>
|
|
@@ -52,6 +52,10 @@ export default class TreeTile extends LightningElement {
|
|
|
52
52
|
return this.treeNode.method && !this.hasChildren;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
private get showForwardArrow() {
|
|
56
|
+
return this.treeNode.showForwardArrow;
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
private get methodText() {
|
|
56
60
|
if (this.treeNode.method && this.treeNode.method in methodTextMap) {
|
|
57
61
|
return methodTextMap[this.treeNode.method];
|
|
@@ -43,3 +43,35 @@ export function pollUntil(
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the same settled value or rejection as `promise`, or rejects after `forMaximumMs`
|
|
49
|
+
* if it does not settle in time. When `forMaximumMs` is omitted or non-positive, returns
|
|
50
|
+
* `promise` unchanged. Clears the timeout when `promise` settles first.
|
|
51
|
+
*/
|
|
52
|
+
export function waitUntilResolved<T>(
|
|
53
|
+
promise: Promise<T>,
|
|
54
|
+
forMaximumMs?: number,
|
|
55
|
+
timeoutMessage = "Timed out waiting for promise to settle."
|
|
56
|
+
): Promise<T> {
|
|
57
|
+
if (forMaximumMs == null || forMaximumMs <= 0) {
|
|
58
|
+
return promise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Promise<T>((resolve, reject) => {
|
|
62
|
+
const timeoutId = setTimeout(() => {
|
|
63
|
+
reject(new Error(timeoutMessage));
|
|
64
|
+
}, forMaximumMs);
|
|
65
|
+
|
|
66
|
+
promise.then(
|
|
67
|
+
(value) => {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
resolve(value);
|
|
70
|
+
},
|
|
71
|
+
(reason: unknown) => {
|
|
72
|
+
clearTimeout(timeoutId);
|
|
73
|
+
reject(reason);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Developer-site implementation of docUtils `LocaleDeps` for the unified
|
|
3
|
+
* hybrid search UI (P4.1). Supplies per-device storage of the user's chosen
|
|
4
|
+
* search language (consent-gated behind OneTrust functional-cookie consent) and
|
|
5
|
+
* a browser-language -> supported-locale resolver.
|
|
6
|
+
*
|
|
7
|
+
* dx has no equivalent of arch/localePreference or arch/browserLocale and no
|
|
8
|
+
* shared consent util, so this mirrors the consent read used elsewhere in the
|
|
9
|
+
* org (the OptanonConsent cookie's `groups` field, functional category C0003).
|
|
10
|
+
* OneTrust's standard functional group id is C0003; "C0003:1" means granted,
|
|
11
|
+
* "C0003:0" means denied. We never persist a preference without an explicit
|
|
12
|
+
* grant.
|
|
13
|
+
*
|
|
14
|
+
* Exposes `searchLocalePrefs`, an object implementing the docUtils LocaleDeps
|
|
15
|
+
* shape ({ getStored, setStored, resolveBrowser }), plus a `buildSearchLocalePrefs`
|
|
16
|
+
* factory for tests / dependency injection.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { LocaleDeps } from "docUtils/searchLocale";
|
|
20
|
+
|
|
21
|
+
export const STORAGE_KEY = "dx-search-language";
|
|
22
|
+
export const FUNCTIONAL_GROUP_ID = "C0003";
|
|
23
|
+
export const DEFAULT_LOCALE = "en-us";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Region/script-aware overrides for cases a plain language-only match can't
|
|
27
|
+
* resolve. Keys are normalized lowercase BCP-47 tags (or language-only codes);
|
|
28
|
+
* values are site locale ids. Order of checks: exact id -> these rules ->
|
|
29
|
+
* language-only first match -> default.
|
|
30
|
+
*
|
|
31
|
+
* Mirrors the product decisions baked into arch/browserLocale:
|
|
32
|
+
* - bare "es" -> es-mx (es-mx is the generic Spanish)
|
|
33
|
+
* - any pt-* -> pt-br (only Brazilian Portuguese is published)
|
|
34
|
+
* - no/nb/nn -> nb-no (only Norwegian id)
|
|
35
|
+
* - Chinese is matched on script/region, never collapsed to language-only.
|
|
36
|
+
*/
|
|
37
|
+
const TAG_OVERRIDES: Record<string, string> = {
|
|
38
|
+
// Chinese — Traditional
|
|
39
|
+
"zh-tw": "zh-tw",
|
|
40
|
+
"zh-hant": "zh-tw",
|
|
41
|
+
"zh-hk": "zh-tw",
|
|
42
|
+
"zh-mo": "zh-tw",
|
|
43
|
+
// Chinese — Simplified (and bare "zh")
|
|
44
|
+
zh: "zh-cn",
|
|
45
|
+
"zh-cn": "zh-cn",
|
|
46
|
+
"zh-hans": "zh-cn",
|
|
47
|
+
"zh-sg": "zh-cn",
|
|
48
|
+
// Spanish
|
|
49
|
+
es: "es-mx",
|
|
50
|
+
"es-es": "es-es",
|
|
51
|
+
// Portuguese (only pt-br exists)
|
|
52
|
+
pt: "pt-br",
|
|
53
|
+
"pt-pt": "pt-br",
|
|
54
|
+
// Norwegian (only nb-no exists)
|
|
55
|
+
no: "nb-no",
|
|
56
|
+
nb: "nb-no",
|
|
57
|
+
nn: "nb-no"
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the OptanonConsent cookie and return whether the functional group is
|
|
62
|
+
* granted. Defensive: if OneTrust hasn't loaded or the cookie is absent
|
|
63
|
+
* (tests, pre-consent, non-prod), returns false so we never persist without an
|
|
64
|
+
* explicit grant.
|
|
65
|
+
*/
|
|
66
|
+
export function hasFunctionalConsent(): boolean {
|
|
67
|
+
try {
|
|
68
|
+
const cookie = document.cookie
|
|
69
|
+
.split("; ")
|
|
70
|
+
.find((row) => row.startsWith("OptanonConsent="));
|
|
71
|
+
if (!cookie) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// The cookie value is URL-encoded querystring-style, e.g.
|
|
75
|
+
// "...&groups=C0001:1,C0003:1,C0002:0&...".
|
|
76
|
+
const value = decodeURIComponent(cookie.split("=").slice(1).join("="));
|
|
77
|
+
const groups = new URLSearchParams(value).get("groups");
|
|
78
|
+
if (!groups) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return groups
|
|
82
|
+
.split(",")
|
|
83
|
+
.some((entry) => entry.trim() === `${FUNCTIONAL_GROUP_ID}:1`);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map a single language-only code (e.g. "ja", "de") to its site locale id by
|
|
91
|
+
* finding the first supported id whose language subtag matches.
|
|
92
|
+
*/
|
|
93
|
+
function languageOnlyMatch(
|
|
94
|
+
language: string,
|
|
95
|
+
supportedIds: string[]
|
|
96
|
+
): string | undefined {
|
|
97
|
+
return supportedIds.find((id) => id.split("-")[0] === language);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the stored language preference, or null if none / unavailable. Reading an
|
|
102
|
+
* already-stored preference is low-risk, so it is NOT consent-gated.
|
|
103
|
+
*/
|
|
104
|
+
export function getStoredLanguage(): string | null {
|
|
105
|
+
try {
|
|
106
|
+
return window.localStorage.getItem(STORAGE_KEY);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Persist the chosen language, but only if the user has granted functional
|
|
114
|
+
* consent. Returns true if the value was written, false if it was skipped (no
|
|
115
|
+
* consent) or failed. When skipped, the choice still applies for the current
|
|
116
|
+
* page/session via in-memory state and the ?lang= URL param.
|
|
117
|
+
*/
|
|
118
|
+
export function setStoredLanguage(language: string): boolean {
|
|
119
|
+
if (!language || !hasFunctionalConsent()) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
window.localStorage.setItem(STORAGE_KEY, language);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the best site locale id for the given browser languages. The first
|
|
132
|
+
* browser language that maps to a supported id wins; earlier entries take
|
|
133
|
+
* precedence over later ones (matching navigator.languages ordering).
|
|
134
|
+
* Returns "en-us" if nothing matches.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveBrowserLocale(
|
|
137
|
+
navigatorLanguages: readonly string[] | undefined,
|
|
138
|
+
supportedIds: string[]
|
|
139
|
+
): string {
|
|
140
|
+
if (!navigatorLanguages || navigatorLanguages.length === 0) {
|
|
141
|
+
return DEFAULT_LOCALE;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const supported = new Set(supportedIds);
|
|
145
|
+
|
|
146
|
+
for (const raw of navigatorLanguages) {
|
|
147
|
+
if (!raw) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const tag = raw.toLowerCase();
|
|
151
|
+
|
|
152
|
+
// 1. Exact id match (e.g. "ja-jp" -> ja-jp).
|
|
153
|
+
if (supported.has(tag)) {
|
|
154
|
+
return tag;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Region/script override (e.g. "zh-tw" -> zh-tw, "es" -> es-mx).
|
|
158
|
+
const overridden = TAG_OVERRIDES[tag];
|
|
159
|
+
if (overridden && supported.has(overridden)) {
|
|
160
|
+
return overridden;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. Language-only fall-through (e.g. "de-ch" -> de -> de-de).
|
|
164
|
+
const language = tag.split("-")[0];
|
|
165
|
+
const overriddenLang = TAG_OVERRIDES[language];
|
|
166
|
+
if (overriddenLang && supported.has(overriddenLang)) {
|
|
167
|
+
return overriddenLang;
|
|
168
|
+
}
|
|
169
|
+
const matched = languageOnlyMatch(language, supportedIds);
|
|
170
|
+
if (matched) {
|
|
171
|
+
return matched;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return DEFAULT_LOCALE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build a docUtils LocaleDeps bundle for the developer site. Exposed as a
|
|
180
|
+
* factory so tests / callers can inject overrides; the default-exported
|
|
181
|
+
* `searchLocalePrefs` is the production singleton.
|
|
182
|
+
*/
|
|
183
|
+
export function buildSearchLocalePrefs(
|
|
184
|
+
overrides: Partial<LocaleDeps> = {}
|
|
185
|
+
): LocaleDeps {
|
|
186
|
+
return {
|
|
187
|
+
getStored: overrides.getStored ?? getStoredLanguage,
|
|
188
|
+
setStored: overrides.setStored ?? setStoredLanguage,
|
|
189
|
+
resolveBrowser: overrides.resolveBrowser ?? resolveBrowserLocale
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const searchLocalePrefs: LocaleDeps = buildSearchLocalePrefs();
|
package/LICENSE
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2020, Salesforce.com, Inc.
|
|
2
|
-
All rights reserved.
|
|
3
|
-
|
|
4
|
-
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
5
|
-
|
|
6
|
-
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
7
|
-
|
|
8
|
-
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
9
|
-
|
|
10
|
-
* Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
11
|
-
|
|
12
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data 360 search API – shared endpoints and helpers for sidebar search,
|
|
3
|
-
* search results page, and has-results check.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const STORAGE_KEY_PREFIX = "data360-search:";
|
|
7
|
-
|
|
8
|
-
/** Data 360 Search API result item (title, url, matchedText) */
|
|
9
|
-
export interface Data360SearchResultItem {
|
|
10
|
-
title?: string;
|
|
11
|
-
url?: string;
|
|
12
|
-
matchedText?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Normalized result shape stored in cache (no selected/select; those are applied on restore). */
|
|
16
|
-
export interface Data360SearchCacheItem {
|
|
17
|
-
title: string;
|
|
18
|
-
titleHighlights: Array<{ length: number; offset: number }>;
|
|
19
|
-
excerpt: string;
|
|
20
|
-
excerptHighlights: Array<{ length: number; offset: number }>;
|
|
21
|
-
uniqueId: string;
|
|
22
|
-
href: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface Data360SearchCache {
|
|
26
|
-
query: string;
|
|
27
|
-
results: Data360SearchCacheItem[];
|
|
28
|
-
/** Timestamp when stored (Date.now()); missing on older entries (treated as stale). */
|
|
29
|
-
storedAt?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const CACHE_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
33
|
-
|
|
34
|
-
export function isCacheStale(cache: Data360SearchCache | null): boolean {
|
|
35
|
-
if (!cache?.storedAt) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
return Date.now() - cache.storedAt > CACHE_MAX_AGE_MS;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getStoredSearch(
|
|
42
|
-
baseUrlPath: string
|
|
43
|
-
): Data360SearchCache | null {
|
|
44
|
-
try {
|
|
45
|
-
const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}${baseUrlPath}`);
|
|
46
|
-
if (!raw) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
const parsed = JSON.parse(raw) as Data360SearchCache;
|
|
50
|
-
if (
|
|
51
|
-
!parsed ||
|
|
52
|
-
typeof parsed.query !== "string" ||
|
|
53
|
-
!Array.isArray(parsed.results)
|
|
54
|
-
) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return parsed;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function setStoredSearch(
|
|
64
|
-
baseUrlPath: string,
|
|
65
|
-
data: Data360SearchCache
|
|
66
|
-
): void {
|
|
67
|
-
try {
|
|
68
|
-
const toStore: Data360SearchCache = {
|
|
69
|
-
...data,
|
|
70
|
-
storedAt: Date.now()
|
|
71
|
-
};
|
|
72
|
-
localStorage.setItem(
|
|
73
|
-
`${STORAGE_KEY_PREFIX}${baseUrlPath}`,
|
|
74
|
-
JSON.stringify(toStore)
|
|
75
|
-
);
|
|
76
|
-
} catch {
|
|
77
|
-
/* ignore quota or disabled localStorage */
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const DATA_360_SEARCH_PATH = "/data-360-search/search";
|
|
82
|
-
export const DATA_360_HAS_RESULTS_PATH = "/data-360-search/has-results";
|
|
83
|
-
export const DATA_360_SEARCH_ORIGIN = "https://developer.salesforce.com";
|
|
84
|
-
|
|
85
|
-
export function getBaseUrlPath(): string {
|
|
86
|
-
const url = DATA_360_SEARCH_ORIGIN + window.location.pathname;
|
|
87
|
-
const lastSlash = url.lastIndexOf("/");
|
|
88
|
-
return lastSlash > 0 ? url.substring(0, lastSlash) : url;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Read search query from URL: ?q=, ?keywords= (migrated to q), or #q=.
|
|
93
|
-
*/
|
|
94
|
-
export function getQueryFromUrl(): string {
|
|
95
|
-
const params = new URLSearchParams(window.location.search);
|
|
96
|
-
const keywords = params.get("keywords");
|
|
97
|
-
if (keywords) {
|
|
98
|
-
const url = new URL(window.location.href);
|
|
99
|
-
url.searchParams.delete("keywords");
|
|
100
|
-
url.searchParams.set("q", keywords);
|
|
101
|
-
window.history.replaceState(null, "", url.href);
|
|
102
|
-
return keywords;
|
|
103
|
-
}
|
|
104
|
-
const qFromSearch = params.get("q");
|
|
105
|
-
if (qFromSearch) {
|
|
106
|
-
return qFromSearch;
|
|
107
|
-
}
|
|
108
|
-
const hash = window.location.hash.slice(1);
|
|
109
|
-
if (hash) {
|
|
110
|
-
try {
|
|
111
|
-
const qFromHash = new URLSearchParams(hash).get("q");
|
|
112
|
-
if (qFromHash) {
|
|
113
|
-
return qFromHash;
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
/* ignore */
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return "";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Quick check if the current page has searchable results.
|
|
124
|
-
* Only when this returns true should the Data 360 sidebar be shown.
|
|
125
|
-
*/
|
|
126
|
-
export async function fetchHasResults(): Promise<boolean> {
|
|
127
|
-
try {
|
|
128
|
-
const res = await fetch(DATA_360_HAS_RESULTS_PATH, {
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: { "Content-Type": "application/json" },
|
|
131
|
-
body: JSON.stringify({ baseUrlPath: getBaseUrlPath() })
|
|
132
|
-
});
|
|
133
|
-
if (!res.ok) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
const data = await res.json();
|
|
137
|
-
return data.hasResults === true;
|
|
138
|
-
} catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Run a Data 360 search. Returns the raw result items; callers normalize for UI.
|
|
145
|
-
*/
|
|
146
|
-
export async function fetchSearch(
|
|
147
|
-
searchQuery: string
|
|
148
|
-
): Promise<Data360SearchResultItem[]> {
|
|
149
|
-
const query = searchQuery.trim();
|
|
150
|
-
if (!query) {
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
|
-
const res = await fetch(DATA_360_SEARCH_PATH, {
|
|
154
|
-
method: "POST",
|
|
155
|
-
headers: { "Content-Type": "application/json" },
|
|
156
|
-
body: JSON.stringify({
|
|
157
|
-
searchQuery: query,
|
|
158
|
-
baseUrlPath: getBaseUrlPath()
|
|
159
|
-
})
|
|
160
|
-
});
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
throw new Error(`Search API error: ${res.status}`);
|
|
163
|
-
}
|
|
164
|
-
const data = await res.json();
|
|
165
|
-
return Array.isArray(data)
|
|
166
|
-
? data
|
|
167
|
-
: data?.results ?? data?.data?.results ?? [];
|
|
168
|
-
}
|