@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.
Files changed (135) hide show
  1. package/lwc.config.json +18 -3
  2. package/package.json +18 -6
  3. package/src/assets/icons/salesforcebrand-sprite/svg/symbols.svg +492 -111
  4. package/src/assets/svg/big-moon.svg +1 -0
  5. package/src/assets/svg/blue-circle.svg +3 -0
  6. package/src/assets/svg/login-widget-bg.png +0 -0
  7. package/src/assets/svg/subscribe-background-left.svg +14 -0
  8. package/src/assets/svg/subscribe-background-right.svg +11 -0
  9. package/src/assets/svg/trial-left.svg +6 -0
  10. package/src/assets/svg/trial-right.svg +26 -0
  11. package/src/modules/dx/banner/banner (1).ts +12 -0
  12. package/src/modules/dx/banner/banner.ts +3 -5
  13. package/src/modules/dx/button/button.css +10 -9
  14. package/src/modules/dx/buttonToggle/buttonToggle.css +50 -0
  15. package/src/modules/dx/buttonToggle/buttonToggle.html +17 -0
  16. package/src/modules/dx/buttonToggle/buttonToggle.ts +75 -0
  17. package/src/modules/dx/cardCallout/cardCallout.ts +8 -4
  18. package/src/modules/dx/cardContent/cardContent.html +4 -1
  19. package/src/modules/dx/cardDocs/cardDocs.html +4 -1
  20. package/src/modules/dx/cardEvent/cardEvent.html +1 -2
  21. package/src/modules/dx/cardEvent/cardEvent.ts +1 -1
  22. package/src/modules/dx/cardExpanded/cardExpanded.html +1 -6
  23. package/src/modules/dx/cardExpanded/cardExpanded.ts +2 -2
  24. package/src/modules/dx/cardMinimal/cardMinimal.html +1 -6
  25. package/src/modules/dx/cardTrial/cardTrial.css +63 -4
  26. package/src/modules/dx/cardTrial/cardTrial.html +96 -12
  27. package/src/modules/dx/cardTrial/cardTrial.ts +70 -2
  28. package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.css +149 -0
  29. package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.html +85 -0
  30. package/src/modules/dx/cardTrialExpanded/cardTrialExpanded.ts +67 -0
  31. package/src/modules/dx/checkbox/checkbox.css +4 -0
  32. package/src/modules/dx/codeBlock/codeBlock.ts +5 -1
  33. package/src/modules/dx/dropdown/dropdown.html +5 -1
  34. package/src/modules/dx/dropdown/dropdown.ts +13 -3
  35. package/src/modules/dx/dropdownOption/dropdownOption.css +119 -1
  36. package/src/modules/dx/dropdownOption/dropdownOption.html +4 -0
  37. package/src/modules/dx/dropdownOption/dropdownOption.ts +20 -7
  38. package/src/modules/dx/feature/feature.ts +3 -3
  39. package/src/modules/dx/featuredContentHeader/featuredContentHeader.css +84 -18
  40. package/src/modules/dx/featuredContentHeader/featuredContentHeader.html +109 -99
  41. package/src/modules/dx/featuredContentHeader/featuredContentHeader.ts +27 -7
  42. package/src/modules/dx/featuresList/featuresList.css +1 -1
  43. package/src/modules/dx/filterMenu/filterMenu.css +26 -8
  44. package/src/modules/dx/filterMenu/filterMenu.html +24 -8
  45. package/src/modules/dx/filterMenu/filterMenu.ts +1 -0
  46. package/src/modules/dx/footer/footer.css +1 -1
  47. package/src/modules/dx/footer/footer.html +25 -20
  48. package/src/modules/dx/footer/footer.ts +19 -15
  49. package/src/modules/dx/footer/links.ts +115 -39
  50. package/src/modules/dx/formattedDateTime/formattedDateTime.ts +8 -3
  51. package/src/modules/dx/header/header.html +10 -3
  52. package/src/modules/dx/header/header.ts +4 -0
  53. package/src/modules/dx/headerMobileNavMenu/headerMobileNavMenu.css +1 -0
  54. package/src/modules/dx/headerSearch/headerSearch.html +0 -1
  55. package/src/modules/dx/hr/hr.css +2 -4
  56. package/src/modules/dx/iconBadge/iconBadge.css +12 -0
  57. package/src/modules/dx/iconBadge/iconBadge.html +2 -7
  58. package/src/modules/dx/iconBadge/iconBadge.ts +15 -5
  59. package/src/modules/dx/imageAndLabel/imageAndLabel.css +0 -1
  60. package/src/modules/dx/input/input.css +17 -1
  61. package/src/modules/dx/input/input.html +5 -1
  62. package/src/modules/dx/input/input.ts +8 -0
  63. package/src/modules/dx/interactiveImage/interactiveImage.css +59 -0
  64. package/src/modules/dx/interactiveImage/interactiveImage.html +32 -0
  65. package/src/modules/dx/interactiveImage/interactiveImage.ts +71 -0
  66. package/src/modules/dx/logo/logo.css +0 -1
  67. package/src/modules/dx/logo/logo.ts +1 -1
  68. package/src/modules/dx/metadataBadge/metadataBadge.css +41 -0
  69. package/src/modules/dx/metadataBadge/metadataBadge.html +5 -0
  70. package/src/modules/dx/metadataBadge/metadataBadge.ts +24 -0
  71. package/src/modules/dx/modal/modal.css +40 -0
  72. package/src/modules/dx/modal/modal.html +10 -0
  73. package/src/modules/dx/modal/modal.ts +50 -0
  74. package/src/modules/dx/modalDrawer/modalDrawer.html +2 -2
  75. package/src/modules/dx/modalDrawer/modalDrawer.ts +7 -1
  76. package/src/modules/dx/pagination/pagination.css +1 -2
  77. package/src/modules/dx/popover/popover.css +28 -3
  78. package/src/modules/dx/popover/popover.html +1 -0
  79. package/src/modules/dx/popover/popover.ts +83 -44
  80. package/src/modules/dx/searchResults/coveo.css +18989 -0
  81. package/src/modules/dx/searchResults/searchResults.css +387 -0
  82. package/src/modules/dx/searchResults/searchResults.html +104 -0
  83. package/src/modules/dx/searchResults/searchResults.ts +187 -0
  84. package/src/modules/dx/section/section.css +10 -1
  85. package/src/modules/dx/section/section.ts +1 -1
  86. package/src/modules/dx/select/select.css +7 -3
  87. package/src/modules/dx/sidebar/sidebar.css +1 -145
  88. package/src/modules/dx/sidebar/sidebar.ts +2 -0
  89. package/src/modules/dx/sidebarOld/sidebarOld.css +1 -145
  90. package/src/modules/dx/sidebarOld/sidebarOld.ts +2 -0
  91. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +23 -3
  92. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +1 -1
  93. package/src/modules/dx/tab/tab.css +0 -4
  94. package/src/modules/dx/tabPanel/tabPanel.css +20 -0
  95. package/src/modules/dx/tabPanel/tabPanel.html +13 -0
  96. package/src/modules/dx/tabPanel/tabPanel.ts +27 -0
  97. package/src/modules/dx/tabPanelItem/tabPanelItem.css +39 -0
  98. package/src/modules/dx/tabPanelItem/tabPanelItem.html +15 -0
  99. package/src/modules/dx/tabPanelItem/tabPanelItem.ts +39 -0
  100. package/src/modules/dx/tabPanelList/tabPanelList.css +35 -0
  101. package/src/modules/dx/tabPanelList/tabPanelList.html +47 -0
  102. package/src/modules/dx/tabPanelList/tabPanelList.ts +164 -0
  103. package/src/modules/dx/tabPanelList/uniqueId.ts +6 -0
  104. package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.css +33 -0
  105. package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.html +53 -0
  106. package/src/modules/dx/tbidAvatarButton/tbidAvatarButton.ts +440 -0
  107. package/src/modules/dx/toc/toc.css +1 -0
  108. package/src/modules/dx/toc/toc.ts +13 -0
  109. package/src/modules/dx/treeItem/treeItem.html +2 -0
  110. package/src/modules/dx/treeItem/treeItem.ts +8 -6
  111. package/src/modules/dx/treeTile/treeTile.css +18 -18
  112. package/src/modules/dx/treeTile/treeTile.html +5 -4
  113. package/src/modules/dx/treeTile/treeTile.ts +17 -3
  114. package/src/modules/dx/typeBadge/typeBadge.css +15 -56
  115. package/src/modules/dx/typeBadge/typeBadge.html +6 -0
  116. package/src/modules/dx/typeBadge/typeBadge.ts +149 -46
  117. package/src/modules/dxBaseElements/archiveCard/archiveCard.ts +2 -6
  118. package/src/modules/dxBaseElements/headerBase/headerBase.ts +11 -1
  119. package/src/modules/dxConstants/brands/brands.ts +14 -0
  120. package/src/modules/dxConstants/colors/colors.ts +14 -0
  121. package/src/modules/dxConstants/contentTypes/contentTypes.ts +10 -0
  122. package/src/modules/dxHelpers/card/card.css +1 -1
  123. package/src/modules/dxHelpers/commonHeader/commonHeader.css +4 -3
  124. package/src/modules/dxHelpers/commonSidebar/commonSidebar.css +145 -0
  125. package/src/modules/dxHelpers/{sidebar/sidebar.css → commonTreeItem/commonTreeItem.css} +0 -0
  126. package/src/modules/dxUtils/async/async.ts +45 -0
  127. package/src/modules/dxUtils/constants/constants.ts +0 -13
  128. package/src/modules/dxUtils/css/css.ts +10 -0
  129. package/src/modules/dxUtils/lwc/lwc.ts +9 -0
  130. package/src/modules/dxUtils/prismjs/prismjs.ts +347 -9
  131. package/src/modules/dxUtils/queryCoordinator/queryCoordinator.ts +37 -11
  132. package/LICENSE +0 -12
  133. package/src/modules/dx/typeBadgeGroup/typeBadgeGroup.css +0 -12
  134. package/src/modules/dx/typeBadgeGroup/typeBadgeGroup.html +0 -11
  135. 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
+ }
@@ -9,6 +9,7 @@
9
9
  display: flex;
10
10
  flex-direction: row-reverse;
11
11
  width: var(--dx-c-toc-width);
12
+ max-width: 100%;
12
13
  word-break: break-word;
13
14
  }
14
15
 
@@ -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, this.isExpanded ? "expand" : "collapse");
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, "click");
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, sideNavAction: SidebarGtmAction) {
141
- track(event.currentTarget!, "custEv_leftNavLinkClick", {
140
+ private sendGtm(event: Event) {
141
+ track(event.currentTarget!, "custEv_leftNavClick", {
142
+ navType: "left nav bar",
142
143
  clickText: this._treeNode.label,
143
- sideNavAction
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/sidebar";
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-type-badge {
149
- --dx-c-type-badge-text-width: 21px;
148
+ dx-metadata-badge {
149
+ --dx-c-metadata-badge-text-width: 21px;
150
150
  }
151
151
 
152
- dx-type-badge.get {
153
- --dx-c-type-badge-color: var(--dx-g-blue-vibrant-50);
154
- --dx-c-type-badge-background: var(--dx-g-blue-vibrant-90);
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-type-badge.put {
158
- --dx-c-type-badge-color: var(--dx-g-yellow-vibrant-40);
159
- --dx-c-type-badge-background: var(--dx-g-yellow-vibrant-90);
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-type-badge.post {
163
- --dx-c-type-badge-color: var(--dx-g-green-vibrant-40);
164
- --dx-c-type-badge-background: var(--dx-g-green-vibrant-90);
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-type-badge.patch {
168
- --dx-c-type-badge-color: var(--dx-g-teal-vibrant-40);
169
- --dx-c-type-badge-background: var(--dx-g-teal-vibrant-90);
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-type-badge.delete {
173
- --dx-c-type-badge-color: var(--dx-g-red-vibrant-40);
174
- --dx-c-type-badge-background: var(--dx-g-hot-orange-vibrant-90);
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-type-badge
19
+ <dx-metadata-badge
20
20
  if:true={showMethod}
21
- class={badgeClass}
21
+ class="flex tile-icon"
22
22
  size="small"
23
23
  value={methodText}
24
- ></dx-type-badge>
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 badgeClass() {
33
- return cx("flex", "tile-icon", this.treeNode.method);
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() {