@salesforcedevs/dx-components 0.73.0 → 0.78.0-avatar-button

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.
@@ -0,0 +1,129 @@
1
+ :host {
2
+ --dx-c-button-custom-color: var(--dx-g-blue-vibrant-50);
3
+ --dx-c-button-custom-background: transparent;
4
+ --dx-c-button-custom-border: 1px solid transparent;
5
+ --dx-c-button-custom-color-hover: var(--dx-g-blue-vibrant-50);
6
+ --dx-c-button-custom-background-hover: var(--dx-g-cloud-blue-vibrant-90);
7
+ --dx-c-button-custom-border-hover: var(--dx-g-cloud-blue-vibrant-90);
8
+ --dx-c-slot-empty-width: max-content;
9
+ }
10
+
11
+ .avatar-small-container {
12
+ padding: 2px 0;
13
+ }
14
+
15
+ .avatar {
16
+ border-radius: 100%;
17
+ border: 2px solid #0d9dda;
18
+ }
19
+
20
+ .avatar.avatar-small {
21
+ display: block;
22
+ height: var(--dx-g-spacing-lg);
23
+ width: var(--dx-g-spacing-lg);
24
+ }
25
+
26
+ .avatar.avatar-medium {
27
+ margin-bottom: 14px;
28
+ width: var(--dx-g-spacing-2xl);
29
+ }
30
+
31
+ .login-container {
32
+ padding: 16px;
33
+ }
34
+
35
+ .login-container.logged-in {
36
+ padding-top: 68px;
37
+ }
38
+
39
+ .header-img {
40
+ left: 0;
41
+ position: absolute;
42
+ top: 0;
43
+ width: 100%;
44
+ z-index: -1;
45
+ }
46
+
47
+ .heading {
48
+ color: var(--dx-g-blue-vibrant-20);
49
+ display: block;
50
+ font-family: var(--dx-g-font-display);
51
+ font-size: var(--dx-g-text-lg);
52
+ line-height: 28px;
53
+ }
54
+
55
+ .sub-heading {
56
+ color: var(--dx-g-blue-vibrant-20);
57
+ display: block;
58
+ font-family: var(--dx-g-font-sans);
59
+ font-size: 12px;
60
+ line-height: 18px;
61
+ padding: 8px 0;
62
+ }
63
+
64
+ ul {
65
+ list-style: none;
66
+ margin: 0;
67
+ padding: 0;
68
+ }
69
+
70
+ .login-list-item {
71
+ padding-bottom: 10px;
72
+ }
73
+
74
+ .login-section ~ .login-section {
75
+ border-top: 1px solid var(--dx-g-gray-90);
76
+ margin-top: 12px;
77
+ }
78
+
79
+ .login-section-title {
80
+ color: var(--dx-g-gray-30);
81
+ display: inline-block;
82
+ font-family: var(--dx-g-font-sans);
83
+ font-size: 14px;
84
+ line-height: 20px;
85
+ padding: 20px 0 12px;
86
+ }
87
+
88
+ .login-link {
89
+ color: var(--dx-g-blue-vibrant-20);
90
+ display: block;
91
+ }
92
+
93
+ .login-link-label {
94
+ display: inline-block;
95
+ font-family: var(--dx-g-font-display);
96
+ font-size: 16px;
97
+ line-height: 24px;
98
+ }
99
+
100
+ .login-link-img {
101
+ display: inline-block;
102
+ height: 16px;
103
+ margin-left: 6px;
104
+ vertical-align: middle;
105
+ }
106
+
107
+ .login-link-icon {
108
+ display: inline-block;
109
+ margin-left: 6px;
110
+ vertical-align: baseline;
111
+ }
112
+
113
+ .login-link-blurb {
114
+ font-family: var(--dx-g-font-sans);
115
+ font-size: 14px;
116
+ line-height: 20px;
117
+ padding-top: 4px;
118
+ }
119
+
120
+ .heading-links {
121
+ color: var(--dx-g-gray-90);
122
+ font-family: var(--dx-g-font-sans);
123
+ font-size: 14px;
124
+ line-height: 20px;
125
+ }
126
+
127
+ a.inline-link {
128
+ color: var(--dx-g-blue-vibrant-50);
129
+ }
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div if:true={isLoggedIn}>
3
+ <dx-popover offset="small" width="320px" open-on-hover>
4
+ <div slot="control" class="avatar-small-container login-control">
5
+ <img src={userInfo.avatarImgSrc} class="avatar avatar-small" />
6
+ </div>
7
+ <div slot="content">
8
+ <div class="login-container logged-in">
9
+ <img
10
+ src="/assets/svg/login-widget-bg.png"
11
+ class="header-img"
12
+ />
13
+ <img
14
+ src={userInfo.avatarImgSrc}
15
+ class="avatar avatar-medium"
16
+ />
17
+ <span class="heading">{userInfo.fullName}</span>
18
+ <span class="sub-heading">{userInfo.username}</span>
19
+ <div class="heading-links">
20
+ <!--
21
+ <a href="#" class="inline-link">Switch account</a>
22
+ &nbsp;&nbsp;|&nbsp;&nbsp;
23
+ -->
24
+ <a
25
+ href="#"
26
+ onclick={handleComponentLogout}
27
+ class="inline-link"
28
+ >
29
+ Logout
30
+ </a>
31
+ </div>
32
+ <div class="login-section">
33
+ <span class="login-section-title">Trailblazer.me</span>
34
+ <ul>
35
+ <li class="login-list-item">
36
+ <a href={settingsUrl} class="login-link">
37
+ <span class="login-link-label">
38
+ Settings
39
+ </span>
40
+ <dx-icon
41
+ class="login-link-icon"
42
+ symbol="new_window"
43
+ size="xsmall"
44
+ ></dx-icon>
45
+ </a>
46
+ </li>
47
+ <li class="login-list-item">
48
+ <a href={profileUrl} class="login-link">
49
+ <span class="login-link-label">
50
+ Profile
51
+ </span>
52
+ <dx-icon
53
+ class="login-link-icon"
54
+ symbol="new_window"
55
+ size="xsmall"
56
+ ></dx-icon>
57
+ </a>
58
+ </li>
59
+ <li class="login-list-item">
60
+ <a href="#" class="login-link">
61
+ <span class="login-link-label">
62
+ Developer Forums
63
+ </span>
64
+ <dx-icon
65
+ class="login-link-icon"
66
+ symbol="new_window"
67
+ size="xsmall"
68
+ ></dx-icon>
69
+ </a>
70
+ <div class="login-link-blurb">
71
+ Our Developer Forums have a new home!&nbsp;
72
+ <a href="#" class="inline-link">
73
+ Learn more
74
+ </a>
75
+ </div>
76
+ </li>
77
+ </ul>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </dx-popover>
82
+ </div>
83
+ <div if:false={isLoggedIn}>
84
+ <dx-popover offset="small" width="320px" open-on-hover>
85
+ <dx-button
86
+ slot="control"
87
+ class="login-control"
88
+ icon-symbol="user"
89
+ icon-position="left"
90
+ loading={isLoading}
91
+ variant="custom"
92
+ >
93
+ Login
94
+ </dx-button>
95
+ <div slot="content">
96
+ <div class="login-container">
97
+ <span class="heading">Login</span>
98
+ <div class="login-section">
99
+ <span class="login-section-title">Products</span>
100
+ <ul>
101
+ <li class="login-list-item">
102
+ <a href="#" class="login-link">
103
+ <span class="login-link-label">
104
+ Salesforce
105
+ </span>
106
+ <img
107
+ class="login-link-img"
108
+ src="/assets/svg/salesforce-cloud.svg"
109
+ alt="Salesforce logo"
110
+ />
111
+ <dx-icon
112
+ class="login-link-icon"
113
+ symbol="new_window"
114
+ size="xsmall"
115
+ ></dx-icon>
116
+ </a>
117
+ </li>
118
+ <li class="login-list-item">
119
+ <a href="#" class="login-link">
120
+ <span class="login-link-label">
121
+ Marketing Cloud
122
+ </span>
123
+ <dx-icon
124
+ class="login-link-icon"
125
+ symbol="new_window"
126
+ size="xsmall"
127
+ ></dx-icon>
128
+ </a>
129
+ </li>
130
+ </ul>
131
+ </div>
132
+ <div class="login-section">
133
+ <span class="login-section-title">
134
+ Community & Resources
135
+ </span>
136
+ <ul>
137
+ <li class="login-list-item">
138
+ <a href={loginUrl} class="login-link">
139
+ <span class="login-link-label">
140
+ Trailblazer.me
141
+ </span>
142
+ </a>
143
+ <div class="login-link-blurb">
144
+ Login into access a rich community of
145
+ Salesforce Developers, get ideas, and find
146
+ solutions.
147
+ </div>
148
+ </li>
149
+ </ul>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </dx-popover>
154
+ </div>
155
+ </template>
@@ -0,0 +1,199 @@
1
+ import { api, LightningElement } from "lwc";
2
+
3
+ const TBID_BASE_URL = "https://dev1-trailblazer-identity.cs192.force.com";
4
+ const TBID_SETTINGS_URL = `${TBID_BASE_URL}/settings`;
5
+ const TBID_PROFILE_URL = `${TBID_BASE_URL}/id`;
6
+ const TBID_API_BASE_URL = "https://development.developer.salesforce.com/tbid";
7
+ const TBID_API_LOGOUT_URL = `${TBID_API_BASE_URL}/logout`;
8
+ const TBID_API_USERINFO_URL = `${TBID_API_BASE_URL}/userinfo`;
9
+ const TBID_API_LOGIN_URL = `${TBID_API_BASE_URL}/dologin`;
10
+ const TBID_API_TOKEN_URL = `${TBID_API_BASE_URL}/token`;
11
+
12
+ declare global {
13
+ interface Window {
14
+ SFIDWidget?: {
15
+ logout(): void;
16
+ login(): void;
17
+ openid_response: any;
18
+ disabled: boolean;
19
+ };
20
+ }
21
+ }
22
+
23
+ export interface UserInfo {
24
+ avatarImgSrc?: string;
25
+ firstName?: string;
26
+ lastName?: string;
27
+ username?: string;
28
+ readonly fullName: string;
29
+ }
30
+
31
+ export default class AvatarButton extends LightningElement {
32
+ @api size: "small" | "medium" | "large" = "medium";
33
+
34
+ private userInfo: UserInfo = {
35
+ get fullName() {
36
+ if (this.firstName && this.lastName) {
37
+ return `${this.firstName} ${this.lastName}`;
38
+ }
39
+ return this.firstName || this.lastName || "";
40
+ }
41
+ };
42
+ private isLoading = false;
43
+ private settingsUrl = TBID_SETTINGS_URL;
44
+ private profileUrl = TBID_PROFILE_URL;
45
+ private _didReceiveUserInfo = false;
46
+
47
+ private get loginUrl() {
48
+ return `${TBID_API_LOGIN_URL}?startURL=${window.encodeURIComponent(
49
+ window.location.href
50
+ )}`;
51
+ }
52
+
53
+ private get isLoggedIn() {
54
+ return this._didReceiveUserInfo;
55
+ }
56
+
57
+ connectedCallback() {
58
+ if (
59
+ new URLSearchParams(window.location.search).get("loginSuccess") ===
60
+ "true"
61
+ ) {
62
+ this.isLoading = true;
63
+ }
64
+
65
+ window.addEventListener("tbid-login", this.handleSsoLogin);
66
+ window.addEventListener("tbid-logout", this.handleSsoLogout);
67
+
68
+ const isWidgetDisabled = window.SFIDWidget?.disabled;
69
+ const isWidgetLoggedIn = window.SFIDWidget?.openid_response;
70
+
71
+ // If the component loads and (1) we are logging in and (2) the embedded login widget script
72
+ // is in the DOM but (3) the widget has either not loaded or not completed login yet, we
73
+ // defer the userInfo request in order to allow the SFIDWidget the time to trigger it (via
74
+ // it's own login handler) after SSO login. If any of (1) - (3) are false, then we request
75
+ // user info immediately. The check prevents duplicate requests.
76
+ if (
77
+ this.isLoading &&
78
+ (!window.SFIDWidget || (!isWidgetDisabled && !isWidgetLoggedIn)) &&
79
+ document.querySelector('script[src*="authProviderEmbeddedLogin"]')
80
+ ) {
81
+ setTimeout(() => {
82
+ if (!window.SFIDWidget?.openid_response) {
83
+ this.requestUserInfo();
84
+ }
85
+ }, 1000);
86
+ } else {
87
+ this.requestUserInfo();
88
+ }
89
+ }
90
+
91
+ disconnectedCallback() {
92
+ window.removeEventListener("tbid-login", this.handleSsoLogin);
93
+ window.removeEventListener("tbid-logout", this.handleSsoLogout);
94
+ }
95
+
96
+ // This handles logout from within this component, rather than from SSO via the SFIDWidget.
97
+ private handleLogout = (isSsoLogout: boolean) => {
98
+ this._didReceiveUserInfo = false;
99
+ this.isLoading = false;
100
+ this.updateAvatarWithUserInfo({}); // clear old info
101
+
102
+ if (!isSsoLogout && window.SFIDWidget?.openid_response) {
103
+ // If the SFIDWidget is around and has an access token, and if logout was not *already*
104
+ // triggered by SSO logout, defer to the SFIDWidget. This will ensure that the SSO
105
+ // session is logged out as well.
106
+ window.SFIDWidget.logout();
107
+ } else {
108
+ // Always clear the session token; if not SSO logout, this will also revoke the token with
109
+ // TBID; if SSO logout, that step is already taken care of
110
+ fetch(`${TBID_API_LOGOUT_URL}?isSsoLogout=${isSsoLogout}`); // no need to await this
111
+ if (!isSsoLogout) {
112
+ const ifr = document.createElement("iframe");
113
+ ifr.setAttribute(
114
+ "src",
115
+ "https://dev1-trailblazer-identity.cs192.force.com/secur/logout.jsp"
116
+ );
117
+ document.body.appendChild(ifr);
118
+ }
119
+ }
120
+ };
121
+
122
+ // This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
123
+ private handleSsoLogout = this.handleLogout.bind(this, true);
124
+
125
+ private handleComponentLogout = this.handleLogout.bind(this, false);
126
+
127
+ // This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
128
+ private handleSsoLogin = async (event: Event) => {
129
+ const { userInfo } = (event as CustomEvent).detail;
130
+ const token = userInfo?.access_token;
131
+
132
+ // `token` should always be defined if an SSO login occurs, but we check for extra safety
133
+ if (token) {
134
+ this.isLoading = true;
135
+ // If an SSO login occurred and we have an SSO access token, we want to start using
136
+ // it rather than some other non-linked access token, for the "seamless SSO"
137
+ // experience
138
+ try {
139
+ await fetch(TBID_API_TOKEN_URL, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json"
143
+ },
144
+ body: JSON.stringify({
145
+ token
146
+ })
147
+ });
148
+ } catch (ex) {
149
+ console.error(`Attempt to update token failed: ${ex}`);
150
+ }
151
+ }
152
+
153
+ if (userInfo) {
154
+ this.updateAvatarWithUserInfo(userInfo);
155
+ this._didReceiveUserInfo = true;
156
+ }
157
+ };
158
+
159
+ private handleComponentLogin = () => {
160
+ if (window.SFIDWidget) {
161
+ // If the SFIDWidget is around, defer to it for login. This will ensure that the SSO
162
+ // session is used if possible.
163
+ window.SFIDWidget.login();
164
+ } else {
165
+ window.location.href = this.loginUrl;
166
+ }
167
+ };
168
+
169
+ private updateAvatarWithUserInfo = (userInfo: any) => {
170
+ if (!userInfo) {
171
+ return;
172
+ }
173
+
174
+ this.userInfo.avatarImgSrc = userInfo.photos?.thumbnail;
175
+ this.userInfo.firstName = userInfo.given_name;
176
+ this.userInfo.lastName = userInfo.family_name;
177
+ this.userInfo.username = userInfo.preferred_username;
178
+ // TODO: Consider displaying initials if no photo. Is there always a photo even if just a default one?
179
+ };
180
+
181
+ private requestUserInfo = async () => {
182
+ this.isLoading = true;
183
+
184
+ try {
185
+ const userInfoRes = await fetch(TBID_API_USERINFO_URL);
186
+
187
+ if (userInfoRes.ok) {
188
+ const userInfo = await userInfoRes.json();
189
+ this.updateAvatarWithUserInfo(userInfo);
190
+ this._didReceiveUserInfo = true;
191
+ }
192
+ } catch (ex) {
193
+ // TODO: Something not directly related to auth went wrong. Unsure what to do here.
194
+ console.error(`Could not request user info: ${ex}`);
195
+ }
196
+
197
+ this.isLoading = false;
198
+ };
199
+ }
@@ -45,7 +45,11 @@ export default class CodeBlock extends LightningElement {
45
45
  { label: "PHP", id: "php" },
46
46
  { label: "JSON", id: "json" },
47
47
  { label: "Swift", id: "swift" },
48
- { label: "Kotlin", id: "kotlin" }
48
+ { label: "Kotlin", id: "kotlin" },
49
+ { label: "Python", id: "python" },
50
+ { label: "Bash", id: "bash" },
51
+ { label: "SQL", id: "sql" },
52
+ { label: "YAML", id: "yaml" }
49
53
  ];
50
54
 
51
55
  connectedCallback() {
@@ -19,6 +19,9 @@
19
19
  onstatechange={handleStateChange}
20
20
  ></dx-header-search>
21
21
  </div>
22
+ <div if:true={showTbidLogin} class="header-tbid-login">
23
+ <dx-avatar-button></dx-avatar-button>
24
+ </div>
22
25
  <div if:true={showSignup} class="header-login-signup">
23
26
  <dx-button
24
27
  aria-label="Sign Up For Salesforce Developer Edition"
@@ -1,6 +1,10 @@
1
1
  import { HeaderBase } from "dxBaseElements/headerBase";
2
2
 
3
3
  export default class Header extends HeaderBase {
4
+ private get showTbidLogin(): boolean {
5
+ return this.showSignup;
6
+ }
7
+
4
8
  private get showSignup(): boolean {
5
9
  return this.signupLink
6
10
  ? (this.mobile && !this.isSearchOpen) || !this.mobile
@@ -3,6 +3,6 @@ import { LightningElement, api } from "lwc";
3
3
  export default class Logo extends LightningElement {
4
4
  @api href: string = "/";
5
5
  @api imgSrc: string = "/assets/svg/salesforce-cloud.svg";
6
- @api imgAlt: string = "Salesforce log";
6
+ @api imgAlt: string = "Salesforce logo";
7
7
  @api label!: string;
8
8
  }
@@ -109,12 +109,14 @@ header.state-show-mobile-nav .header_l2_group-nav_overflow {
109
109
  margin-left: var(--dx-g-spacing-sm);
110
110
  }
111
111
 
112
- .header-login-signup {
112
+ .header-login-signup,
113
+ .header-tbid-login {
113
114
  display: flex;
114
115
  align-items: center;
115
116
  }
116
117
 
117
- .header-login-signup dx-button {
118
+ .header-login-signup dx-button,
119
+ .header-tbid-login dx-avatar-button {
118
120
  margin-left: var(--dx-g-spacing-smd);
119
121
  }
120
122