@salesforcedevs/dx-components 0.56.1 → 0.56.2-example
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 +18 -3
- package/package.json +18 -6
- package/src/assets/icons/salesforcebrand-sprite/svg/symbols.svg +492 -111
- package/src/assets/svg/big-moon.svg +1 -0
- package/src/assets/svg/blue-circle.svg +3 -0
- package/src/assets/svg/login-widget-bg.png +0 -0
- package/src/assets/svg/subscribe-background-left.svg +14 -0
- package/src/assets/svg/subscribe-background-right.svg +11 -0
- package/src/assets/svg/trial-left.svg +6 -0
- package/src/assets/svg/trial-right.svg +26 -0
- package/src/modules/dx/banner/banner (1).ts +12 -0
- package/src/modules/dx/banner/banner.ts +3 -5
- package/src/modules/dx/button/button.css +10 -9
- package/src/modules/dx/buttonToggle/buttonToggle.css +50 -0
- package/src/modules/dx/buttonToggle/buttonToggle.html +17 -0
- package/src/modules/dx/buttonToggle/buttonToggle.ts +75 -0
- package/src/modules/dx/cardCallout/cardCallout.ts +8 -4
- package/src/modules/dx/cardContent/cardContent.html +4 -1
- package/src/modules/dx/cardDocs/cardDocs.html +4 -1
- package/src/modules/dx/cardEvent/cardEvent.html +1 -2
- package/src/modules/dx/cardEvent/cardEvent.ts +1 -1
- package/src/modules/dx/cardExpanded/cardExpanded.html +1 -6
- package/src/modules/dx/cardExpanded/cardExpanded.ts +2 -2
- package/src/modules/dx/cardMinimal/cardMinimal.html +1 -6
- package/src/modules/dx/cardTrial/cardTrial.css +63 -4
- package/src/modules/dx/cardTrial/cardTrial.html +96 -12
- package/src/modules/dx/cardTrial/cardTrial.ts +70 -2
- package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.css +149 -0
- package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.html +85 -0
- package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.ts +67 -0
- package/src/modules/dx/checkbox/checkbox.css +4 -0
- package/src/modules/dx/codeBlock/codeBlock.ts +5 -1
- package/src/modules/dx/dropdown/dropdown.html +5 -1
- package/src/modules/dx/dropdown/dropdown.ts +13 -3
- package/src/modules/dx/dropdownOption/dropdownOption.css +119 -1
- package/src/modules/dx/dropdownOption/dropdownOption.html +4 -0
- package/src/modules/dx/dropdownOption/dropdownOption.ts +20 -7
- package/src/modules/dx/feature/feature.ts +3 -3
- package/src/modules/dx/featuredContentHeader/featuredContentHeader.css +84 -18
- package/src/modules/dx/featuredContentHeader/featuredContentHeader.html +109 -99
- package/src/modules/dx/featuredContentHeader/featuredContentHeader.ts +27 -7
- package/src/modules/dx/featuresList/featuresList.css +1 -1
- package/src/modules/dx/filterMenu/filterMenu.css +26 -8
- package/src/modules/dx/filterMenu/filterMenu.html +24 -8
- package/src/modules/dx/filterMenu/filterMenu.ts +1 -0
- package/src/modules/dx/footer/footer.css +1 -1
- package/src/modules/dx/footer/footer.html +25 -20
- package/src/modules/dx/footer/footer.ts +19 -15
- package/src/modules/dx/footer/links.ts +115 -39
- package/src/modules/dx/formattedDateTime/formattedDateTime.ts +8 -3
- package/src/modules/dx/header/header.html +10 -3
- package/src/modules/dx/header/header.ts +4 -0
- package/src/modules/dx/headerMobileNavMenu/headerMobileNavMenu.css +1 -0
- package/src/modules/dx/headerSearch/headerSearch.html +0 -1
- package/src/modules/dx/hr/hr.css +2 -4
- package/src/modules/dx/iconBadge/iconBadge.css +12 -0
- package/src/modules/dx/iconBadge/iconBadge.html +2 -7
- package/src/modules/dx/iconBadge/iconBadge.ts +15 -5
- package/src/modules/dx/imageAndLabel/imageAndLabel.css +0 -1
- package/src/modules/dx/input/input.css +17 -1
- package/src/modules/dx/input/input.html +5 -1
- package/src/modules/dx/input/input.ts +8 -0
- package/src/modules/dx/interactiveImage/interactiveImage.css +59 -0
- package/src/modules/dx/interactiveImage/interactiveImage.html +32 -0
- package/src/modules/dx/interactiveImage/interactiveImage.ts +71 -0
- package/src/modules/dx/logo/logo.css +0 -1
- package/src/modules/dx/logo/logo.ts +1 -1
- package/src/modules/dx/metadataBadge/metadataBadge.css +41 -0
- package/src/modules/dx/metadataBadge/metadataBadge.html +5 -0
- package/src/modules/dx/metadataBadge/metadataBadge.ts +24 -0
- package/src/modules/dx/modal/modal.css +40 -0
- package/src/modules/dx/modal/modal.html +10 -0
- package/src/modules/dx/modal/modal.ts +50 -0
- package/src/modules/dx/modalDrawer/modalDrawer.html +2 -2
- package/src/modules/dx/modalDrawer/modalDrawer.ts +7 -1
- package/src/modules/dx/pagination/pagination.css +1 -2
- package/src/modules/dx/popover/popover.css +28 -3
- package/src/modules/dx/popover/popover.html +1 -0
- package/src/modules/dx/popover/popover.ts +83 -44
- package/src/modules/dx/searchResults/coveo.css +18989 -0
- package/src/modules/dx/searchResults/searchResults.css +387 -0
- package/src/modules/dx/searchResults/searchResults.html +104 -0
- package/src/modules/dx/searchResults/searchResults.ts +187 -0
- package/src/modules/dx/section/section.css +10 -1
- package/src/modules/dx/section/section.ts +1 -1
- package/src/modules/dx/select/select.css +7 -3
- package/src/modules/dx/sidebar/sidebar.css +1 -145
- package/src/modules/dx/sidebar/sidebar.ts +2 -0
- package/src/modules/dx/sidebarOld/sidebarOld.css +1 -145
- package/src/modules/dx/sidebarOld/sidebarOld.ts +2 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +23 -3
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +1 -1
- package/src/modules/dx/tab/tab.css +0 -4
- package/src/modules/dx/tabPanel/tabPanel.css +20 -0
- package/src/modules/dx/tabPanel/tabPanel.html +13 -0
- package/src/modules/dx/tabPanel/tabPanel.ts +27 -0
- package/src/modules/dx/tabPanelItem/tabPanelItem.css +39 -0
- package/src/modules/dx/tabPanelItem/tabPanelItem.html +15 -0
- package/src/modules/dx/tabPanelItem/tabPanelItem.ts +39 -0
- package/src/modules/dx/tabPanelList/tabPanelList.css +35 -0
- package/src/modules/dx/tabPanelList/tabPanelList.html +47 -0
- package/src/modules/dx/tabPanelList/tabPanelList.ts +164 -0
- package/src/modules/dx/tabPanelList/uniqueId.ts +6 -0
- package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.css +33 -0
- package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.html +53 -0
- package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.ts +440 -0
- package/src/modules/dx/toc/toc.css +1 -0
- package/src/modules/dx/toc/toc.ts +13 -0
- package/src/modules/dx/treeItem/treeItem.html +2 -0
- package/src/modules/dx/treeItem/treeItem.ts +8 -6
- package/src/modules/dx/treeTile/treeTile.css +18 -18
- package/src/modules/dx/treeTile/treeTile.html +5 -4
- package/src/modules/dx/treeTile/treeTile.ts +17 -3
- package/src/modules/dx/typeBadge/typeBadge.css +15 -56
- package/src/modules/dx/typeBadge/typeBadge.html +6 -0
- package/src/modules/dx/typeBadge/typeBadge.ts +149 -46
- package/src/modules/dxBaseElements/archiveCard/archiveCard.ts +2 -6
- package/src/modules/dxBaseElements/headerBase/headerBase.ts +11 -1
- package/src/modules/dxConstants/brands/brands.ts +14 -0
- package/src/modules/dxConstants/colors/colors.ts +14 -0
- package/src/modules/dxConstants/contentTypes/contentTypes.ts +10 -0
- package/src/modules/dxHelpers/card/card.css +1 -1
- package/src/modules/dxHelpers/commonHeader/commonHeader.css +4 -3
- package/src/modules/dxHelpers/commonSidebar/commonSidebar.css +145 -0
- package/src/modules/dxHelpers/{sidebar/sidebar.css → commonTreeItem/commonTreeItem.css} +0 -0
- package/src/modules/dxUtils/async/async.ts +45 -0
- package/src/modules/dxUtils/constants/constants.ts +0 -13
- package/src/modules/dxUtils/css/css.ts +10 -0
- package/src/modules/dxUtils/lwc/lwc.ts +9 -0
- package/src/modules/dxUtils/prismjs/prismjs.ts +347 -9
- package/src/modules/dxUtils/queryCoordinator/queryCoordinator.ts +37 -11
- package/LICENSE +0 -12
- package/src/modules/dx/typeBadgeGroup/typeBadgeGroup.css +0 -12
- package/src/modules/dx/typeBadgeGroup/typeBadgeGroup.html +0 -11
- package/src/modules/dx/typeBadgeGroup/typeBadgeGroup.ts +0 -18
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { api, LightningElement } from "lwc";
|
|
2
|
+
import Cookies from "js-cookie";
|
|
3
|
+
import defaults from "lodash.defaults";
|
|
4
|
+
import { pollUntil } from "dxUtils/async";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
SFIDWidget?: {
|
|
9
|
+
isAlive({ callback }: { callback: (...args: any[]) => any }): void;
|
|
10
|
+
logout(): void;
|
|
11
|
+
login(): void;
|
|
12
|
+
openid_response: any;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UserInfo {
|
|
19
|
+
avatarImgSrc?: string;
|
|
20
|
+
company?: string;
|
|
21
|
+
firstName?: string;
|
|
22
|
+
id?: string;
|
|
23
|
+
lastName?: string;
|
|
24
|
+
orgId?: string;
|
|
25
|
+
relationship?: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
username?: string;
|
|
28
|
+
fullName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface EventSourceEvent extends Event {
|
|
32
|
+
id: string;
|
|
33
|
+
retry?: number;
|
|
34
|
+
data: string;
|
|
35
|
+
event?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const enum PlatformEventsType {
|
|
39
|
+
UserDataChange = "UserDataChange",
|
|
40
|
+
UserPhotoChange = "UserPhotoChange",
|
|
41
|
+
UserMerge = "UserMerge"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// This component handles the core Salesforce OAuth 2.0 flow used by TBID for login, rendering a
|
|
45
|
+
// simple Login button or avatar icon depending on login status. It should be reusable in any case
|
|
46
|
+
// where TBID login is needed.
|
|
47
|
+
export default class TbidAvatarButton extends LightningElement {
|
|
48
|
+
@api tbidBaseUrl = "";
|
|
49
|
+
@api tbidApiBaseUrl = "";
|
|
50
|
+
|
|
51
|
+
@api login = (event: Event) => this.handleComponentLogin(event);
|
|
52
|
+
@api logout = (event: Event) => this.handleComponentLogout(event);
|
|
53
|
+
|
|
54
|
+
private userInfo: UserInfo = {};
|
|
55
|
+
private isLoading = false;
|
|
56
|
+
private eventSource?: EventSource;
|
|
57
|
+
private _isRequestingUserInfo = false;
|
|
58
|
+
private _didReceiveUserInfo = false;
|
|
59
|
+
|
|
60
|
+
// _didReceiveUserInfo is a proxy for "isLoggedIn" because we only want to show the UI corresponding
|
|
61
|
+
// to logged-in state when user info has been received (avoids flashes of weird content)
|
|
62
|
+
private get isLoggedIn() {
|
|
63
|
+
return this._didReceiveUserInfo;
|
|
64
|
+
}
|
|
65
|
+
private get tbidIframeUrl() {
|
|
66
|
+
return `${this.tbidBaseUrl}/secur/logout.jsp`;
|
|
67
|
+
}
|
|
68
|
+
private get tbidApiLogoutUrl() {
|
|
69
|
+
return `${this.tbidApiBaseUrl}/logout`;
|
|
70
|
+
}
|
|
71
|
+
private get tbidApiUserInfoUrl() {
|
|
72
|
+
return `${this.tbidApiBaseUrl}/userinfo`;
|
|
73
|
+
}
|
|
74
|
+
private get tbidApiTokenUrl() {
|
|
75
|
+
return `${this.tbidApiBaseUrl}/token`;
|
|
76
|
+
}
|
|
77
|
+
private get tbidApiPlatformEventsUrl() {
|
|
78
|
+
return `${this.tbidApiBaseUrl}/platform-events`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async connectedCallback() {
|
|
82
|
+
// Backend API for TBID login sets this state cookie if successful login occurs.
|
|
83
|
+
const isLoginSuccessRedirect =
|
|
84
|
+
Cookies.get("tbidLoginSuccess") === "true";
|
|
85
|
+
|
|
86
|
+
// The SFIDWidget enables seamless SSO in Chrome, so we will defer to it for login in cases where it is present and enabled.
|
|
87
|
+
const isWidgetDisabled = window.SFIDWidget?.disabled;
|
|
88
|
+
const isWidgetLoggedIn = window.SFIDWidget?.openid_response;
|
|
89
|
+
const isWidgetMaybeLoading =
|
|
90
|
+
!isWidgetDisabled &&
|
|
91
|
+
!isWidgetLoggedIn &&
|
|
92
|
+
Boolean(
|
|
93
|
+
// eslint-disable-next-line @lwc/lwc/no-document-query
|
|
94
|
+
document.querySelector(
|
|
95
|
+
'script[src*="authProviderEmbeddedLogin"]'
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
window.addEventListener("tbid-login", this.handleSsoLogin);
|
|
100
|
+
window.addEventListener("tbid-logout", this.handleSsoLogout);
|
|
101
|
+
|
|
102
|
+
this.isLoading = true;
|
|
103
|
+
|
|
104
|
+
if (!isLoginSuccessRedirect) {
|
|
105
|
+
// Not a login redirect, no further work to do right now except try to request userInfo (in case a session already exists)
|
|
106
|
+
await this.requestUserInfo();
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!this._didReceiveUserInfo &&
|
|
110
|
+
isWidgetMaybeLoading &&
|
|
111
|
+
!window.SFIDWidget?.openid_response &&
|
|
112
|
+
window.SFIDWidget?.isAlive
|
|
113
|
+
) {
|
|
114
|
+
// The initial userinfo request failed, but SFIDWidget was maybe loading, and it
|
|
115
|
+
// still hasn't completed login, so we want to check whether it's alive and wait
|
|
116
|
+
// (up to a maximum of 1 second) for it to possibly finish logging in before we
|
|
117
|
+
// turn off the "loading" indicator (this prevents odd flashes of content).
|
|
118
|
+
await this.waitForSFIDWidget();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.isLoading = false;
|
|
122
|
+
} else {
|
|
123
|
+
// We got here from a login success redirect
|
|
124
|
+
Cookies.remove("tbidLoginSuccess"); // first, cleanup
|
|
125
|
+
|
|
126
|
+
if (isWidgetMaybeLoading) {
|
|
127
|
+
// This is a successful login and we have an SFIDWidget present, so it *should*
|
|
128
|
+
// handle finishing up login itself. But it has a maximum of 1 second to do so.
|
|
129
|
+
const didWidgetLoad = await this.waitForSFIDWidget();
|
|
130
|
+
if (!didWidgetLoad) {
|
|
131
|
+
// SFIDWidget not responding in time; complete login ourselves.
|
|
132
|
+
await this.requestUserInfo();
|
|
133
|
+
this.dispatchLoginSuccess();
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// No SFIDWidget loading, proceed immediately to completing normal initialization.
|
|
137
|
+
await this.requestUserInfo();
|
|
138
|
+
this.dispatchLoginSuccess();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.isLoading = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
disconnectedCallback() {
|
|
146
|
+
window.removeEventListener("tbid-login", this.handleSsoLogin);
|
|
147
|
+
window.removeEventListener("tbid-logout", this.handleSsoLogout);
|
|
148
|
+
this.teardownEventSource();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The SFIDWidget requires third party cookies to function. In browsers with third party
|
|
152
|
+
// cookies disabled, the widget will load but never log in, resulting in delayed UI updates
|
|
153
|
+
// in some cases. To mitigate this, this method calls the widget's "isAlive" method, which
|
|
154
|
+
// should respond very quickly if third party cookies are enabled. If it does respond quickly,
|
|
155
|
+
// then this method waits a maximum of 1 second for the widget to finish logging in. If it does
|
|
156
|
+
// *not* respond quickly, the method resolves early so that we can stop waiting pointlessly.
|
|
157
|
+
private waitForSFIDWidget = async () => {
|
|
158
|
+
const widgetNotAlivePromise = new Promise<boolean>((resolve) => {
|
|
159
|
+
const timeoutId = setTimeout(() => resolve(false), 100);
|
|
160
|
+
window.SFIDWidget?.isAlive({
|
|
161
|
+
// If the widget responds as expected, this promise will fail to resolve and thus
|
|
162
|
+
// we will continue to wait on the other promise, below.
|
|
163
|
+
callback: () => clearTimeout(timeoutId)
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
const widgetLoggedInPromise = pollUntil(
|
|
167
|
+
() => Boolean(window.SFIDWidget?.openid_response),
|
|
168
|
+
100,
|
|
169
|
+
1000
|
|
170
|
+
);
|
|
171
|
+
const success = await Promise.race([
|
|
172
|
+
widgetNotAlivePromise,
|
|
173
|
+
widgetLoggedInPromise
|
|
174
|
+
]);
|
|
175
|
+
return success;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
private setupEventSource = () => {
|
|
179
|
+
// subscribe to platform events
|
|
180
|
+
this.eventSource = new EventSource(this.tbidApiPlatformEventsUrl);
|
|
181
|
+
this.eventSource.addEventListener(
|
|
182
|
+
PlatformEventsType.UserDataChange,
|
|
183
|
+
// eslint-disable-next-line no-undef
|
|
184
|
+
this.handleUserDataChange as EventListener
|
|
185
|
+
);
|
|
186
|
+
this.eventSource.addEventListener(
|
|
187
|
+
PlatformEventsType.UserMerge,
|
|
188
|
+
this.handleSsoLogout
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
private teardownEventSource = () => {
|
|
193
|
+
if (!this.eventSource) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.eventSource.removeEventListener(
|
|
198
|
+
PlatformEventsType.UserDataChange,
|
|
199
|
+
// eslint-disable-next-line no-undef
|
|
200
|
+
this.handleUserDataChange as EventListener
|
|
201
|
+
);
|
|
202
|
+
this.eventSource.removeEventListener(
|
|
203
|
+
PlatformEventsType.UserMerge,
|
|
204
|
+
this.handleSsoLogout
|
|
205
|
+
);
|
|
206
|
+
this.eventSource.close();
|
|
207
|
+
this.eventSource = undefined;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
private handleLogout = (isSsoLogout: boolean) => {
|
|
211
|
+
// Reset state
|
|
212
|
+
this._didReceiveUserInfo = false;
|
|
213
|
+
this.isLoading = false;
|
|
214
|
+
this.updateUserInfo({}); // clear old info
|
|
215
|
+
this.teardownEventSource();
|
|
216
|
+
this.dispatchLogoutSuccess();
|
|
217
|
+
|
|
218
|
+
if (!isSsoLogout && window.SFIDWidget?.openid_response) {
|
|
219
|
+
// If the SFIDWidget is around and has an access token, and if logout was not *already*
|
|
220
|
+
// triggered by SSO logout, defer to the SFIDWidget. This will ensure that the SSO
|
|
221
|
+
// session is logged out as well.
|
|
222
|
+
window.SFIDWidget.logout();
|
|
223
|
+
} else {
|
|
224
|
+
// Always clear the session token; if not SSO logout, this will also revoke the token with
|
|
225
|
+
// TBID; if SSO logout, that step is already taken care of. No need to await this.
|
|
226
|
+
fetch(`${this.tbidApiLogoutUrl}?isSsoLogout=${isSsoLogout}`, {
|
|
227
|
+
method: "DELETE",
|
|
228
|
+
credentials: "same-origin"
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!isSsoLogout) {
|
|
232
|
+
// Dropping an iframe is required to fully get TBID to destroy the session; this is
|
|
233
|
+
// a TBID issue and they have requested that we do this for now.
|
|
234
|
+
const ifr = document.createElement("iframe");
|
|
235
|
+
ifr.setAttribute("src", this.tbidIframeUrl);
|
|
236
|
+
document.body.appendChild(ifr);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
|
|
242
|
+
private handleSsoLogout = this.handleLogout.bind(this, true);
|
|
243
|
+
|
|
244
|
+
// This handles logout from _within_ this component ("Logout" click), rather than from SSO via the SFIDWidget.
|
|
245
|
+
private handleComponentLogout = (event: Event) => {
|
|
246
|
+
event?.preventDefault();
|
|
247
|
+
this.handleLogout(false);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
|
|
251
|
+
private handleSsoLogin = async (event: Event) => {
|
|
252
|
+
const { userInfo } = (event as CustomEvent).detail;
|
|
253
|
+
const token = userInfo?.access_token;
|
|
254
|
+
|
|
255
|
+
// `token` should always be defined if an SSO login occurs, but we'll be extra safe.
|
|
256
|
+
if (!token) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// If an SSO login occurred and we have an SSO access token, we want to start using
|
|
261
|
+
// it rather than some other non-linked access token, for the "seamless SSO"
|
|
262
|
+
// experience. The `token` endpoint will invalidate any other existing token.
|
|
263
|
+
try {
|
|
264
|
+
const tokenResponse = await fetch(this.tbidApiTokenUrl, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json"
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
token
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (tokenResponse.ok) {
|
|
275
|
+
// With a new token, we want to re-establish the eventSource connection and treat as new login.
|
|
276
|
+
this.teardownEventSource();
|
|
277
|
+
this.setupEventSource();
|
|
278
|
+
// TODO: It sucks that we have to re-request user info here, since it should come with the token
|
|
279
|
+
// on SSO login. For some reason, the user info on SSO login lacks the user's photo URLs, so we
|
|
280
|
+
// have to re-request it. Maybe something to bring up with the TBID team.
|
|
281
|
+
await this.requestUserInfo();
|
|
282
|
+
this.dispatchLoginSuccess();
|
|
283
|
+
} else if (
|
|
284
|
+
tokenResponse.status === 409 &&
|
|
285
|
+
!this._didReceiveUserInfo
|
|
286
|
+
) {
|
|
287
|
+
// Valid token received, but we still need to grab userInfo. This could happen in certain edge cases
|
|
288
|
+
// involving "seamless SSO" (e.g., you might be sitting on our page in more than one tab, then log in
|
|
289
|
+
// on one of them; in that case, our back-end will receive the token from one tab, end up _rejecting_ it
|
|
290
|
+
// as a duplicate on other tabs where the login event occurs, and yet still need to request user info
|
|
291
|
+
// on those other tabs).
|
|
292
|
+
await this.requestUserInfo();
|
|
293
|
+
}
|
|
294
|
+
} catch (ex) {
|
|
295
|
+
console.error(`Attempt to update token failed: ${ex}`);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// This handles logout from _within_ this component ("Trailblazer.me" click), rather than from SSO via the SFIDWidget.
|
|
300
|
+
private handleComponentLogin = (event: Event) => {
|
|
301
|
+
if (window.SFIDWidget) {
|
|
302
|
+
// If the SFIDWidget is around, defer to it for login. This will ensure that the SSO
|
|
303
|
+
// session is used if possible.
|
|
304
|
+
event.preventDefault(); // don't navigate; the widget will handle that instead.
|
|
305
|
+
window.SFIDWidget.login();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
private handleUserDataChange = ({ data }: EventSourceEvent) => {
|
|
310
|
+
let parsedEventData: any;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
parsedEventData = JSON.parse(data);
|
|
314
|
+
} catch (ex) {
|
|
315
|
+
console.error(
|
|
316
|
+
`Unparseable ${PlatformEventsType.UserDataChange} data received`
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.updateUserInfo(parsedEventData);
|
|
322
|
+
this.dispatchUserInfo();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// This is the only method that should manipulate this.userInfo.
|
|
326
|
+
private updateUserInfo = (userInfo: any) => {
|
|
327
|
+
if (!userInfo) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (Object.keys(userInfo).length === 0) {
|
|
332
|
+
// This is a "clear" call.
|
|
333
|
+
this.userInfo = {};
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const dirtyNextUserInfo: UserInfo = {
|
|
338
|
+
avatarImgSrc: userInfo.photos?.thumbnail,
|
|
339
|
+
firstName: userInfo.given_name,
|
|
340
|
+
lastName: userInfo.family_name,
|
|
341
|
+
username: userInfo.preferred_username,
|
|
342
|
+
id: userInfo.user_id,
|
|
343
|
+
orgId: userInfo.organization_id,
|
|
344
|
+
fullName:
|
|
345
|
+
userInfo.given_name && userInfo.family_name
|
|
346
|
+
? `${userInfo.given_name} ${userInfo.family_name}`
|
|
347
|
+
: userInfo.given_name || userInfo.family_name || ""
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
if (userInfo.custom_attributes) {
|
|
351
|
+
// ProfilePictureUrl updates earlier than photos.thumbnail, so we prefer it if it's available here.
|
|
352
|
+
dirtyNextUserInfo.avatarImgSrc =
|
|
353
|
+
userInfo.custom_attributes.ProfilePictureUrl ||
|
|
354
|
+
dirtyNextUserInfo.avatarImgSrc;
|
|
355
|
+
dirtyNextUserInfo.company = userInfo.custom_attributes.CompanyName;
|
|
356
|
+
dirtyNextUserInfo.relationship =
|
|
357
|
+
userInfo.custom_attributes.RelationshipToSalesforce;
|
|
358
|
+
dirtyNextUserInfo.role = userInfo.custom_attributes.Role;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cleanNextUserInfo: UserInfo = {};
|
|
362
|
+
|
|
363
|
+
Object.entries(dirtyNextUserInfo).forEach(([key, val]) => {
|
|
364
|
+
// uncleanUserInfo may have undefined values which we don't want in the final product
|
|
365
|
+
if (!val) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let cleanVal = val;
|
|
370
|
+
|
|
371
|
+
// Design requests that we not show the @trailblazer.me part of the username
|
|
372
|
+
if (key === "username" && typeof val === "string") {
|
|
373
|
+
const regExpMatch = val.match("(.*)@trailblazer.me$");
|
|
374
|
+
if (regExpMatch && regExpMatch[1]) {
|
|
375
|
+
cleanVal = regExpMatch[1];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
cleanNextUserInfo[key as keyof UserInfo] = cleanVal;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Keep the old info for any values _not_ defined on `cleanUserInfo`
|
|
383
|
+
this.userInfo = defaults(cleanNextUserInfo, this.userInfo);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
private requestUserInfo = async () => {
|
|
387
|
+
if (this._isRequestingUserInfo) {
|
|
388
|
+
// prevent duplicate requests
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this._isRequestingUserInfo = true;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const userInfoRes = await fetch(this.tbidApiUserInfoUrl);
|
|
396
|
+
|
|
397
|
+
if (userInfoRes.ok) {
|
|
398
|
+
if (!this.eventSource) {
|
|
399
|
+
this.setupEventSource();
|
|
400
|
+
}
|
|
401
|
+
const userInfo = await userInfoRes.json();
|
|
402
|
+
this.updateUserInfo(userInfo);
|
|
403
|
+
this._didReceiveUserInfo = true;
|
|
404
|
+
this.dispatchUserInfo();
|
|
405
|
+
}
|
|
406
|
+
} catch (ex) {
|
|
407
|
+
// Something not directly related to auth went wrong. Unsure what to do here.
|
|
408
|
+
console.error(`Could not request user info: ${ex}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this._isRequestingUserInfo = false;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
private dispatchUserInfo = () => {
|
|
415
|
+
this.dispatchEvent(
|
|
416
|
+
new CustomEvent("tbiduserinfo", {
|
|
417
|
+
bubbles: true,
|
|
418
|
+
detail: {
|
|
419
|
+
userInfo: this.userInfo
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
private dispatchLoginSuccess = () => {
|
|
426
|
+
this.dispatchEvent(
|
|
427
|
+
new CustomEvent("tbidloginsuccess", {
|
|
428
|
+
bubbles: true
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
private dispatchLogoutSuccess = () => {
|
|
434
|
+
this.dispatchEvent(
|
|
435
|
+
new CustomEvent("tbidlogoutsuccess", {
|
|
436
|
+
bubbles: true
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -2,6 +2,7 @@ import { LightningElement, api, track } from "lwc";
|
|
|
2
2
|
import cx from "classnames";
|
|
3
3
|
import { ContentElement } from "typings/custom";
|
|
4
4
|
import { toJson } from "dxUtils/normalizers";
|
|
5
|
+
import { track as sendGtm } from "dxUtils/analytics";
|
|
5
6
|
|
|
6
7
|
export default class Toc extends LightningElement {
|
|
7
8
|
@api title!: string;
|
|
@@ -65,10 +66,22 @@ export default class Toc extends LightningElement {
|
|
|
65
66
|
private onClick(e: Event) {
|
|
66
67
|
const target = e.currentTarget as HTMLElement;
|
|
67
68
|
const id = target.getAttribute("contentid");
|
|
69
|
+
const text = target.getAttribute("data-text");
|
|
70
|
+
const href = target.getAttribute("href");
|
|
71
|
+
|
|
68
72
|
this.dispatchEvent(
|
|
69
73
|
new CustomEvent("selectedcontent", { detail: { name: id } })
|
|
70
74
|
);
|
|
75
|
+
|
|
71
76
|
this._value = id!;
|
|
77
|
+
|
|
78
|
+
sendGtm(e.currentTarget!, "custEv_tableOfContentsClick", {
|
|
79
|
+
clickText: text,
|
|
80
|
+
clickUrl: href,
|
|
81
|
+
elementType: "link",
|
|
82
|
+
locationOnPage: "table of contents",
|
|
83
|
+
itemTitle: this.title
|
|
84
|
+
});
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
private toggleShowContent() {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
is-selected={isSelected}
|
|
10
10
|
oniconclick={onIconClick}
|
|
11
11
|
show-arrow={isParent}
|
|
12
|
+
target={target}
|
|
12
13
|
></dx-tree-tile>
|
|
13
14
|
</a>
|
|
14
15
|
<img
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
key={child.key}
|
|
24
25
|
tree-node={child}
|
|
25
26
|
selected-key={selectedKey}
|
|
27
|
+
parent-name={treeNode.label}
|
|
26
28
|
></dx-tree-item>
|
|
27
29
|
</template>
|
|
28
30
|
</template>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
2
|
import { InternalTreeNode } from "typings/custom";
|
|
3
3
|
import { track } from "dxUtils/analytics";
|
|
4
|
-
import { SidebarGtmAction } from "typings/custom";
|
|
5
4
|
import { isInViewport } from "dxUtils/browser";
|
|
6
5
|
|
|
7
6
|
const DEFAULT_TARGET = "_self";
|
|
@@ -9,6 +8,7 @@ const DEFAULT_TARGET = "_self";
|
|
|
9
8
|
export default class TreeItem extends LightningElement {
|
|
10
9
|
@api selectedKey?: string;
|
|
11
10
|
@api isRoot: boolean = false;
|
|
11
|
+
@api parentName?: string = "";
|
|
12
12
|
|
|
13
13
|
@api
|
|
14
14
|
public get treeNode() {
|
|
@@ -94,7 +94,7 @@ export default class TreeItem extends LightningElement {
|
|
|
94
94
|
private onIconClick(event: CustomEvent): void {
|
|
95
95
|
const isSelectAction = false;
|
|
96
96
|
this.doExpand(isSelectAction);
|
|
97
|
-
this.sendGtm(event
|
|
97
|
+
this.sendGtm(event);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
private onLinkClick(event: Event): void {
|
|
@@ -107,7 +107,7 @@ export default class TreeItem extends LightningElement {
|
|
|
107
107
|
this.doExpand(isSelectAction);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
this.sendGtm(event
|
|
110
|
+
this.sendGtm(event);
|
|
111
111
|
this.fireEvent("tileselected", { name: this._treeNode.name });
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -137,10 +137,12 @@ export default class TreeItem extends LightningElement {
|
|
|
137
137
|
this.isExpanded = value;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
private sendGtm(event: Event
|
|
141
|
-
track(event.currentTarget!, "
|
|
140
|
+
private sendGtm(event: Event) {
|
|
141
|
+
track(event.currentTarget!, "custEv_leftNavClick", {
|
|
142
|
+
navType: "left nav bar",
|
|
142
143
|
clickText: this._treeNode.label,
|
|
143
|
-
|
|
144
|
+
clickUrl: this.href,
|
|
145
|
+
navItem: this.parentName
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
@import "dxHelpers/reset";
|
|
2
|
-
@import "dxHelpers/
|
|
2
|
+
@import "dxHelpers/commonTreeItem";
|
|
3
3
|
|
|
4
4
|
:host {
|
|
5
5
|
display: block;
|
|
@@ -145,33 +145,33 @@
|
|
|
145
145
|
--indentation: calc(20 * var(--base-indentation));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
dx-
|
|
149
|
-
--dx-c-
|
|
148
|
+
dx-metadata-badge {
|
|
149
|
+
--dx-c-metadata-badge-text-width: 21px;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
dx-
|
|
153
|
-
--dx-c-
|
|
154
|
-
--dx-c-
|
|
152
|
+
dx-metadata-badge.get {
|
|
153
|
+
--dx-c-metadata-badge-color: var(--dx-g-blue-vibrant-50);
|
|
154
|
+
--dx-c-metadata-badge-background: var(--dx-g-blue-vibrant-90);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
dx-
|
|
158
|
-
--dx-c-
|
|
159
|
-
--dx-c-
|
|
157
|
+
dx-metadata-badge.put {
|
|
158
|
+
--dx-c-metadata-badge-color: var(--dx-g-yellow-vibrant-40);
|
|
159
|
+
--dx-c-metadata-badge-background: var(--dx-g-yellow-vibrant-90);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
dx-
|
|
163
|
-
--dx-c-
|
|
164
|
-
--dx-c-
|
|
162
|
+
dx-metadata-badge.post {
|
|
163
|
+
--dx-c-metadata-badge-color: var(--dx-g-green-vibrant-40);
|
|
164
|
+
--dx-c-metadata-badge-background: var(--dx-g-green-vibrant-90);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
dx-
|
|
168
|
-
--dx-c-
|
|
169
|
-
--dx-c-
|
|
167
|
+
dx-metadata-badge.patch {
|
|
168
|
+
--dx-c-metadata-badge-color: var(--dx-g-teal-vibrant-40);
|
|
169
|
+
--dx-c-metadata-badge-background: var(--dx-g-teal-vibrant-90);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
dx-
|
|
173
|
-
--dx-c-
|
|
174
|
-
--dx-c-
|
|
172
|
+
dx-metadata-badge.delete {
|
|
173
|
+
--dx-c-metadata-badge-color: var(--dx-g-red-vibrant-40);
|
|
174
|
+
--dx-c-metadata-badge-background: var(--dx-g-hot-orange-vibrant-90);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
a {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<a href={href} class={tileClass}>
|
|
2
|
+
<a href={href} class={tileClass} target={target}>
|
|
3
3
|
<div class="flex indentation" aria-level={tileAriaLevel}>
|
|
4
4
|
<span
|
|
5
5
|
class="tile-label sidebar-item-truncate-text"
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
onclick={onClickIcon}
|
|
17
17
|
></dx-icon>
|
|
18
18
|
</div>
|
|
19
|
-
<dx-
|
|
19
|
+
<dx-metadata-badge
|
|
20
20
|
if:true={showMethod}
|
|
21
|
-
class=
|
|
21
|
+
class="flex tile-icon"
|
|
22
22
|
size="small"
|
|
23
23
|
value={methodText}
|
|
24
|
-
|
|
24
|
+
color={metadataBadgeColor}
|
|
25
|
+
></dx-metadata-badge>
|
|
25
26
|
</div>
|
|
26
27
|
</a>
|
|
27
28
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
2
|
import cx from "classnames";
|
|
3
|
-
import { InternalTreeNode } from "typings/custom";
|
|
3
|
+
import { Color, InternalTreeNode } from "typings/custom";
|
|
4
4
|
|
|
5
5
|
const methodTextMap: {
|
|
6
6
|
[key: string]: string;
|
|
@@ -12,6 +12,16 @@ const methodTextMap: {
|
|
|
12
12
|
delete: "DELE"
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const metadataBadgeColorMap: {
|
|
16
|
+
[key: string]: Color;
|
|
17
|
+
} = {
|
|
18
|
+
get: "blue",
|
|
19
|
+
put: "yellow",
|
|
20
|
+
post: "green",
|
|
21
|
+
patch: "teal",
|
|
22
|
+
delete: "red"
|
|
23
|
+
};
|
|
24
|
+
|
|
15
25
|
export default class TreeTile extends LightningElement {
|
|
16
26
|
@api isRoot = false;
|
|
17
27
|
@api treeNode!: InternalTreeNode;
|
|
@@ -20,6 +30,7 @@ export default class TreeTile extends LightningElement {
|
|
|
20
30
|
@api isSelected = false;
|
|
21
31
|
@api href!: string;
|
|
22
32
|
@api showArrow = false;
|
|
33
|
+
@api target!: string;
|
|
23
34
|
|
|
24
35
|
get tileAriaLevel() {
|
|
25
36
|
return this.treeNode.level + 2;
|
|
@@ -29,8 +40,11 @@ export default class TreeTile extends LightningElement {
|
|
|
29
40
|
return this.isRoot ? "small" : "xsmall";
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
private get
|
|
33
|
-
|
|
43
|
+
private get metadataBadgeColor(): string | undefined {
|
|
44
|
+
if (this.treeNode.method) {
|
|
45
|
+
return metadataBadgeColorMap[this.treeNode.method];
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
private get showMethod() {
|