@vtex/faststore-plugin-buyer-portal 1.3.45 → 1.3.47
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/CHANGELOG.md +15 -1
- package/package.json +1 -1
- package/public/buyer-portal-icons.svg +35 -13
- package/src/features/org-units/clients/OrgUnitClient.ts +17 -0
- package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx +5 -2
- package/src/features/org-units/components/AuthSetupDrawer/AuthSetupDrawer.tsx +346 -0
- package/src/features/org-units/components/AuthSetupDrawer/auth-setup-drawer.scss +138 -0
- package/src/features/org-units/components/AuthSetupDrawer/index.ts +1 -0
- package/src/features/org-units/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +30 -5
- package/src/features/org-units/components/OrgUnitDetailsDropdownMenu/OrgUnitDetailsDropdownMenu.tsx +80 -0
- package/src/features/org-units/components/OrgUnitDetailsDropdownMenu/index.ts +4 -0
- package/src/features/org-units/components/OrgUnitsDropdownMenu/OrgUnitsDropdownMenu.tsx +5 -2
- package/src/features/org-units/components/index.ts +8 -0
- package/src/features/org-units/hooks/index.ts +2 -0
- package/src/features/org-units/hooks/useGetOrgUnitSettings.ts +20 -0
- package/src/features/org-units/hooks/useUpdateOrgUnitSettings.ts +27 -0
- package/src/features/org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +23 -1
- package/src/features/org-units/layouts/OrgUnitDetailsLayout/org-units-details.scss +1 -0
- package/src/features/org-units/services/get-org-unit-settings.service.ts +13 -0
- package/src/features/org-units/services/index.ts +8 -0
- package/src/features/org-units/services/update-org-unit-settings.service.ts +17 -0
- package/src/features/org-units/types/OrgUnitSettings.ts +25 -0
- package/src/features/org-units/types/index.ts +2 -0
- package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +5 -0
- package/src/features/shared/components/Toast/Toast.tsx +43 -2
- package/src/features/shared/components/Toast/toast.scss +23 -5
- package/src/features/shared/components/index.ts +1 -0
- package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +13 -0
- package/src/features/shared/utils/constants.ts +2 -2
- package/src/features/shared/utils/withBuyerPortal.tsx +4 -1
- package/src/features/users/clients/UsersClient.ts +105 -4
- package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +1 -1
- package/src/features/users/components/CreateUserDrawerSelector/CreateUserDrawerSelector.tsx +20 -0
- package/src/features/users/components/CreateUserDrawerWithUsername/CreateUserDrawerWithUsername.tsx +696 -0
- package/src/features/users/components/CreateUserDrawerWithUsername/create-user-drawer-with-username.scss +116 -0
- package/src/features/users/components/UserDropdownMenu/user-dropdown-menu.scss +1 -0
- package/src/features/users/components/UsersCard/UsersCard.tsx +2 -2
- package/src/features/users/components/index.ts +5 -0
- package/src/features/users/hooks/index.ts +2 -0
- package/src/features/users/hooks/useAddUserToOrgUnit.ts +12 -5
- package/src/features/users/hooks/useResetPassword.ts +39 -0
- package/src/features/users/hooks/useValidateUsername.ts +38 -0
- package/src/features/users/layouts/UserDetailsLayout/UserDetailsLayout.tsx +10 -0
- package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +55 -10
- package/src/features/users/layouts/UsersLayout/users-layout.scss +19 -0
- package/src/features/users/services/add-user-to-org-unit.service.ts +8 -6
- package/src/features/users/services/get-users-by-org-unit-id.service.ts +1 -0
- package/src/features/users/services/index.ts +10 -0
- package/src/features/users/services/reset-password.service.ts +24 -0
- package/src/features/users/services/validate-username.service.ts +25 -0
- package/src/features/users/types/UserData.ts +1 -0
- package/src/features/users/types/UserDataService.ts +1 -0
- package/src/pages/org-unit-details.tsx +20 -2
package/src/features/users/components/CreateUserDrawerWithUsername/CreateUserDrawerWithUsername.tsx
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/router";
|
|
4
|
+
|
|
5
|
+
import { Button, IconButton, useUI } from "@faststore/ui";
|
|
6
|
+
|
|
7
|
+
// TODO: Uncomment when 2FA settings are ready
|
|
8
|
+
// import { useGetOrgUnitSettings } from "../../../org-units/hooks";
|
|
9
|
+
import {
|
|
10
|
+
type BasicDrawerProps,
|
|
11
|
+
BasicDrawer,
|
|
12
|
+
ErrorMessage,
|
|
13
|
+
Icon,
|
|
14
|
+
InputText,
|
|
15
|
+
} from "../../../shared/components";
|
|
16
|
+
import { useAnalytics, useBuyerPortal } from "../../../shared/hooks";
|
|
17
|
+
import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
|
|
18
|
+
import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
|
|
19
|
+
import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
|
|
20
|
+
import {
|
|
21
|
+
useAddUserToOrgUnit,
|
|
22
|
+
useResetPassword,
|
|
23
|
+
useValidateUsername,
|
|
24
|
+
} from "../../hooks";
|
|
25
|
+
|
|
26
|
+
import type { RoleData } from "../../../roles/types";
|
|
27
|
+
|
|
28
|
+
type DrawerState =
|
|
29
|
+
| "CREATE_USER"
|
|
30
|
+
| "CONFIRM_SHARED_EMAIL"
|
|
31
|
+
| "ACCESS_TOKEN_DISPLAY";
|
|
32
|
+
|
|
33
|
+
export type CreateUserDrawerProps = Omit<BasicDrawerProps, "children"> & {
|
|
34
|
+
onCreate?: () => void;
|
|
35
|
+
orgUnit: { id: string; name: string };
|
|
36
|
+
rolesOptions: RoleData[] | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const generateUsernameSuggestion = (email: string): string => {
|
|
40
|
+
if (!email || !email.includes("@")) return "";
|
|
41
|
+
return email.split("@")[0];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const CreateUserDrawerWithUsername = ({
|
|
45
|
+
close,
|
|
46
|
+
onCreate,
|
|
47
|
+
orgUnit: { id: orgUnitId },
|
|
48
|
+
rolesOptions,
|
|
49
|
+
...props
|
|
50
|
+
}: CreateUserDrawerProps) => {
|
|
51
|
+
const { pushToast } = useUI();
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
const { featureFlags } = useBuyerPortal();
|
|
54
|
+
const { trackEntityCreated, trackEntityCreateError } = useAnalytics({
|
|
55
|
+
entityType: "user",
|
|
56
|
+
defaultTimerName: "user_creation",
|
|
57
|
+
shouldTrackDefaultTimer: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// TODO[2FA]: Uncomment when 2FA settings are ready
|
|
61
|
+
// const { settings } = useGetOrgUnitSettings(orgUnitId);
|
|
62
|
+
|
|
63
|
+
const [drawerState, setDrawerState] = useState<DrawerState>("CREATE_USER");
|
|
64
|
+
const [accessToken, setAccessToken] = useState<string>("");
|
|
65
|
+
const [hasTokenBeenCopied, setHasTokenBeenCopied] = useState(false);
|
|
66
|
+
|
|
67
|
+
const [sharedEmailData, setSharedEmailData] = useState<{
|
|
68
|
+
email: string;
|
|
69
|
+
existingUserName: string;
|
|
70
|
+
}>({
|
|
71
|
+
email: "",
|
|
72
|
+
existingUserName: "",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const [form, setForm] = useState<{
|
|
76
|
+
name: string;
|
|
77
|
+
userName: string;
|
|
78
|
+
email: string;
|
|
79
|
+
phone: string;
|
|
80
|
+
roles: number[];
|
|
81
|
+
}>({
|
|
82
|
+
name: "",
|
|
83
|
+
userName: "",
|
|
84
|
+
email: "",
|
|
85
|
+
phone: "",
|
|
86
|
+
roles: [],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const [userNameSuggestions, setUsernameSuggestions] = useState<string[]>([]);
|
|
90
|
+
const [showUsernameSuggestions, setShowUsernameSuggestions] = useState(false);
|
|
91
|
+
const [isUsernameValid, setIsUsernameValid] = useState<boolean | null>(null);
|
|
92
|
+
|
|
93
|
+
const [isTouched, setIsTouched] = useState(false);
|
|
94
|
+
const [isUsernameTouched, setIsUsernameTouched] = useState(false);
|
|
95
|
+
|
|
96
|
+
const { name, userName, email, roles, phone } = form;
|
|
97
|
+
|
|
98
|
+
const updateField = (
|
|
99
|
+
field: keyof typeof form,
|
|
100
|
+
value: string | string[] | number[]
|
|
101
|
+
) => {
|
|
102
|
+
setForm((prev) => ({
|
|
103
|
+
...prev,
|
|
104
|
+
[field]: value,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
if (field === "userName") {
|
|
108
|
+
setIsUsernameValid(null);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleValidateUsernameSuccess = useCallback(
|
|
113
|
+
(data: { valid: boolean; userNameSuggestions?: string[] }) => {
|
|
114
|
+
if (data.valid) {
|
|
115
|
+
setIsUsernameValid(true);
|
|
116
|
+
setShowUsernameSuggestions(false);
|
|
117
|
+
} else {
|
|
118
|
+
setIsUsernameValid(false);
|
|
119
|
+
if (data.userNameSuggestions && data.userNameSuggestions.length > 0) {
|
|
120
|
+
setUsernameSuggestions(data.userNameSuggestions);
|
|
121
|
+
setShowUsernameSuggestions(true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const handleValidateUsernameError = useCallback(() => {
|
|
129
|
+
setIsUsernameValid(false);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const { validateUsername, isValidateUsernameLoading } = useValidateUsername({
|
|
133
|
+
onSuccess: handleValidateUsernameSuccess,
|
|
134
|
+
onError: handleValidateUsernameError,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const handleResetPasswordSuccess = useCallback(
|
|
138
|
+
(data: { accessCode: string }) => {
|
|
139
|
+
setAccessToken(data.accessCode);
|
|
140
|
+
setDrawerState("ACCESS_TOKEN_DISPLAY");
|
|
141
|
+
},
|
|
142
|
+
[]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const handleResetPasswordError = useCallback(() => {
|
|
146
|
+
pushToast({
|
|
147
|
+
message:
|
|
148
|
+
"Failed to generate access code. Please try resetting the password later.",
|
|
149
|
+
status: "ERROR",
|
|
150
|
+
});
|
|
151
|
+
onCreate?.();
|
|
152
|
+
close();
|
|
153
|
+
}, [pushToast, onCreate, close]);
|
|
154
|
+
|
|
155
|
+
const { resetPassword, isResetPasswordLoading } = useResetPassword({
|
|
156
|
+
onSuccess: handleResetPasswordSuccess,
|
|
157
|
+
onError: handleResetPasswordError,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (email && email.includes("@") && !userName) {
|
|
162
|
+
const suggestion = generateUsernameSuggestion(email);
|
|
163
|
+
if (suggestion) {
|
|
164
|
+
updateField("userName", suggestion);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, [email]);
|
|
168
|
+
|
|
169
|
+
const handleUsernameBlur = useCallback(() => {
|
|
170
|
+
setIsUsernameTouched(true);
|
|
171
|
+
if (userName?.trim()) {
|
|
172
|
+
validateUsername({
|
|
173
|
+
orgUnitId,
|
|
174
|
+
userName: userName.trim(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}, [userName, orgUnitId, validateUsername]);
|
|
178
|
+
|
|
179
|
+
const handleRefreshUsernameSuggestions = useCallback(() => {
|
|
180
|
+
if (userName?.trim()) {
|
|
181
|
+
validateUsername({
|
|
182
|
+
orgUnitId,
|
|
183
|
+
userName: userName.trim(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}, [userName, orgUnitId, validateUsername]);
|
|
187
|
+
|
|
188
|
+
const handleSelectUsernameSuggestion = (suggestion: string) => {
|
|
189
|
+
updateField("userName", suggestion);
|
|
190
|
+
setShowUsernameSuggestions(false);
|
|
191
|
+
setIsUsernameValid(null);
|
|
192
|
+
validateUsername({
|
|
193
|
+
orgUnitId,
|
|
194
|
+
userName: suggestion,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleAddUserSuccess = ({ user }: { user: { id: string } }) => {
|
|
199
|
+
trackEntityCreated(ANALYTICS_EVENTS.USER_CREATED, "user", {
|
|
200
|
+
user_name: name,
|
|
201
|
+
user_userName: userName,
|
|
202
|
+
user_email: email,
|
|
203
|
+
user_phone: phone,
|
|
204
|
+
roles_count: roles.length,
|
|
205
|
+
role_ids: roles,
|
|
206
|
+
org_unit_id: orgUnitId,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const shouldResetPassword =
|
|
210
|
+
!email?.trim() || drawerState === "CONFIRM_SHARED_EMAIL";
|
|
211
|
+
|
|
212
|
+
if (shouldResetPassword) {
|
|
213
|
+
resetPassword({
|
|
214
|
+
orgUnitId,
|
|
215
|
+
userId: user.id,
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pushToast({
|
|
221
|
+
title: "User added successfully",
|
|
222
|
+
message: `An access code has been sent to [${email}](mailto:${email}) for first-time login.`,
|
|
223
|
+
status: "INFO",
|
|
224
|
+
icon: (
|
|
225
|
+
<button
|
|
226
|
+
data-fs-bp-toast-view-button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => {
|
|
229
|
+
window.location.href = buyerPortalRoutes.userDetails({
|
|
230
|
+
userId: user.id,
|
|
231
|
+
orgUnitId: orgUnitId,
|
|
232
|
+
});
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
View
|
|
236
|
+
</button>
|
|
237
|
+
),
|
|
238
|
+
});
|
|
239
|
+
onCreate?.();
|
|
240
|
+
close();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const { addUserToOrgUnit, isAddUserToOrgUnitLoading } = useAddUserToOrgUnit({
|
|
244
|
+
enableUsernameCreation: featureFlags?.enableUsernameCreation,
|
|
245
|
+
onSuccess: handleAddUserSuccess,
|
|
246
|
+
onError: (err) => {
|
|
247
|
+
let error: {
|
|
248
|
+
message: string;
|
|
249
|
+
orgUnit?: string;
|
|
250
|
+
code?: string;
|
|
251
|
+
user?: {
|
|
252
|
+
id: string;
|
|
253
|
+
email: string;
|
|
254
|
+
name: string;
|
|
255
|
+
};
|
|
256
|
+
existingUserName?: string;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
error = JSON.parse(err.message);
|
|
261
|
+
} catch {
|
|
262
|
+
error = { message: err.message };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
trackEntityCreateError(ANALYTICS_EVENTS.USER_CREATE_ERROR, err, {
|
|
266
|
+
org_unit_id: orgUnitId,
|
|
267
|
+
error_type: error.code || "unknown",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (error.code === "EMAIL_ALREADY_EXISTS") {
|
|
271
|
+
setSharedEmailData({
|
|
272
|
+
email: email,
|
|
273
|
+
existingUserName: error.existingUserName || "another user",
|
|
274
|
+
});
|
|
275
|
+
setDrawerState("CONFIRM_SHARED_EMAIL");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (error.code === "USERNAME_ALREADY_EXISTS") {
|
|
280
|
+
setIsUsernameValid(false);
|
|
281
|
+
handleRefreshUsernameSuggestions();
|
|
282
|
+
pushToast({
|
|
283
|
+
message: "Username is already taken. Please choose another one.",
|
|
284
|
+
status: "ERROR",
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pushToast({
|
|
290
|
+
message: error.message || "An error occurred while creating the user",
|
|
291
|
+
status: "ERROR",
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// TODO[2FA]: Uncomment when 2FA settings are ready
|
|
297
|
+
// const is2FAEnabled = settings?.["2FA"]?.verificationCode ?? false;
|
|
298
|
+
|
|
299
|
+
const isFormValid = () => {
|
|
300
|
+
if (!userName?.trim()) return false;
|
|
301
|
+
|
|
302
|
+
if (roles.length === 0) return false;
|
|
303
|
+
|
|
304
|
+
if (isUsernameValid === false) return false;
|
|
305
|
+
|
|
306
|
+
// TODO[2FA]: Uncomment when 2FA settings are ready
|
|
307
|
+
// if (is2FAEnabled && !email?.trim() && !phone?.trim()) return false;
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleConfirmClick = () => {
|
|
313
|
+
setIsTouched(true);
|
|
314
|
+
setIsUsernameTouched(true);
|
|
315
|
+
|
|
316
|
+
if (!isFormValid()) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (isUsernameValid === null && userName?.trim()) {
|
|
321
|
+
validateUsername({
|
|
322
|
+
orgUnitId,
|
|
323
|
+
userName: userName.trim(),
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
addUserToOrgUnit({
|
|
329
|
+
name,
|
|
330
|
+
userName: userName,
|
|
331
|
+
email: email || undefined,
|
|
332
|
+
phone: phone || undefined,
|
|
333
|
+
roles,
|
|
334
|
+
orgUnitId,
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleConfirmSharedEmail = () => {
|
|
339
|
+
addUserToOrgUnit({
|
|
340
|
+
name,
|
|
341
|
+
userName: userName,
|
|
342
|
+
email,
|
|
343
|
+
phone: phone || undefined,
|
|
344
|
+
roles,
|
|
345
|
+
orgUnitId,
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const handleCopyAccessToken = async () => {
|
|
350
|
+
try {
|
|
351
|
+
await navigator.clipboard.writeText(accessToken);
|
|
352
|
+
setHasTokenBeenCopied(true);
|
|
353
|
+
pushToast({
|
|
354
|
+
message: "Access code copied to clipboard",
|
|
355
|
+
status: "INFO",
|
|
356
|
+
});
|
|
357
|
+
} catch {
|
|
358
|
+
pushToast({
|
|
359
|
+
message: "Failed to copy access code",
|
|
360
|
+
status: "ERROR",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const handleCloseAccessTokenModal = () => {
|
|
366
|
+
if (!hasTokenBeenCopied) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
onCreate?.();
|
|
370
|
+
close();
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const isConfirmButtonEnabled =
|
|
374
|
+
isFormValid() && !isAddUserToOrgUnitLoading && isUsernameValid !== false;
|
|
375
|
+
|
|
376
|
+
const backToCreateUser = () => {
|
|
377
|
+
setDrawerState("CREATE_USER");
|
|
378
|
+
setIsTouched(false);
|
|
379
|
+
setIsUsernameTouched(false);
|
|
380
|
+
setForm({
|
|
381
|
+
name: "",
|
|
382
|
+
userName: "",
|
|
383
|
+
email: "",
|
|
384
|
+
phone: "",
|
|
385
|
+
roles: [],
|
|
386
|
+
});
|
|
387
|
+
setIsUsernameValid(null);
|
|
388
|
+
setUsernameSuggestions([]);
|
|
389
|
+
setShowUsernameSuggestions(false);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const hasRolesError = !rolesOptions || rolesOptions.length === 0;
|
|
393
|
+
|
|
394
|
+
const handleRetryRoles = () => {
|
|
395
|
+
router.reload();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const getDrawerTitle = () => {
|
|
399
|
+
switch (drawerState) {
|
|
400
|
+
case "CONFIRM_SHARED_EMAIL":
|
|
401
|
+
return (
|
|
402
|
+
<>
|
|
403
|
+
<button type="button" onClick={backToCreateUser}>
|
|
404
|
+
<Icon
|
|
405
|
+
name="Back"
|
|
406
|
+
height={20}
|
|
407
|
+
width={20}
|
|
408
|
+
data-fs-bp-create-user-drawer-back-icon
|
|
409
|
+
/>
|
|
410
|
+
</button>
|
|
411
|
+
Confirm shared email
|
|
412
|
+
</>
|
|
413
|
+
);
|
|
414
|
+
case "ACCESS_TOKEN_DISPLAY":
|
|
415
|
+
return "";
|
|
416
|
+
default:
|
|
417
|
+
return "Add User";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<BasicDrawer data-fs-bp-create-user-drawer close={close} {...props}>
|
|
423
|
+
<BasicDrawer.Heading title={getDrawerTitle()} onClose={close} />
|
|
424
|
+
|
|
425
|
+
{drawerState === "CREATE_USER" && (
|
|
426
|
+
<>
|
|
427
|
+
<BasicDrawer.Body>
|
|
428
|
+
{hasRolesError ? (
|
|
429
|
+
<div data-fs-bp-create-user-error-state>
|
|
430
|
+
<Icon name="Warning" height={48} width={48} />
|
|
431
|
+
<h3>Something went wrong</h3>
|
|
432
|
+
<p>The add user form could not be loaded.</p>
|
|
433
|
+
<Button variant="secondary" onClick={handleRetryRoles}>
|
|
434
|
+
Try again
|
|
435
|
+
</Button>
|
|
436
|
+
</div>
|
|
437
|
+
) : (
|
|
438
|
+
<>
|
|
439
|
+
{/* TODO: Uncomment when 2FA settings are ready
|
|
440
|
+
{settings?.["2FA"]?.verificationCode && (
|
|
441
|
+
<div data-fs-bp-create-user-2fa-disclaimer>
|
|
442
|
+
<Icon name="2FAEnabled" height={13} width={16} />
|
|
443
|
+
<p>
|
|
444
|
+
Two-factor authentication is enabled. All users must have
|
|
445
|
+
an email or phone number registered to authenticate.
|
|
446
|
+
</p>
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
*/}
|
|
450
|
+
|
|
451
|
+
<InputText
|
|
452
|
+
label="Full Name (optional)"
|
|
453
|
+
value={name}
|
|
454
|
+
onChange={(event) => updateField("name", event.target.value)}
|
|
455
|
+
/>
|
|
456
|
+
|
|
457
|
+
<InputText
|
|
458
|
+
// TODO[2FA]: Uncomment when 2FA settings are ready
|
|
459
|
+
// label={is2FAEnabled ? "Email" : "Email (optional)"}
|
|
460
|
+
// hasError={is2FAEnabled && isTouched && !email?.trim() && !phone?.trim()}
|
|
461
|
+
label="Email (optional)"
|
|
462
|
+
value={email}
|
|
463
|
+
wrapperProps={{ style: { marginTop: 16 } }}
|
|
464
|
+
onChange={(event) => updateField("email", event.target.value)}
|
|
465
|
+
/>
|
|
466
|
+
|
|
467
|
+
<InputText
|
|
468
|
+
// TODO[2FA]: Uncomment when 2FA settings are ready
|
|
469
|
+
// label={is2FAEnabled ? "Phone number" : "Phone number (optional)"}
|
|
470
|
+
// hasError={is2FAEnabled && isTouched && !email?.trim() && !phone?.trim()}
|
|
471
|
+
label="Phone number (optional)"
|
|
472
|
+
value={phone}
|
|
473
|
+
wrapperProps={{ style: { marginTop: 16 } }}
|
|
474
|
+
onChange={(event) =>
|
|
475
|
+
// TODO: Update this when implementing i18n
|
|
476
|
+
updateField(
|
|
477
|
+
"phone",
|
|
478
|
+
maskPhoneNumber(event.target.value, "USA")
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
/>
|
|
482
|
+
|
|
483
|
+
{/* TODO: Uncomment when 2FA settings are ready
|
|
484
|
+
<ErrorMessage
|
|
485
|
+
show={
|
|
486
|
+
is2FAEnabled &&
|
|
487
|
+
isTouched &&
|
|
488
|
+
!email?.trim() &&
|
|
489
|
+
!phone?.trim()
|
|
490
|
+
}
|
|
491
|
+
message="Email or phone number is required for two-factor authentication"
|
|
492
|
+
/>
|
|
493
|
+
*/}
|
|
494
|
+
|
|
495
|
+
<div data-fs-bp-create-user-userName-wrapper>
|
|
496
|
+
<div data-fs-bp-create-user-userName-suggestions-wrapper>
|
|
497
|
+
<InputText
|
|
498
|
+
label="Username"
|
|
499
|
+
value={userName}
|
|
500
|
+
hasError={
|
|
501
|
+
(isUsernameTouched && !userName?.trim()) ||
|
|
502
|
+
isUsernameValid === false
|
|
503
|
+
}
|
|
504
|
+
onChange={(event) =>
|
|
505
|
+
updateField("userName", event.target.value)
|
|
506
|
+
}
|
|
507
|
+
onBlur={handleUsernameBlur}
|
|
508
|
+
/>
|
|
509
|
+
|
|
510
|
+
{showUsernameSuggestions &&
|
|
511
|
+
userNameSuggestions.length > 0 && (
|
|
512
|
+
<IconButton
|
|
513
|
+
data-fs-bp-create-user-userName-suggestions-icon
|
|
514
|
+
icon={<Icon name="Refresh" width={20} height={20} />}
|
|
515
|
+
size="small"
|
|
516
|
+
aria-label={"refresh userName suggestions"}
|
|
517
|
+
onClick={() => {
|
|
518
|
+
handleRefreshUsernameSuggestions();
|
|
519
|
+
}}
|
|
520
|
+
/>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
{showUsernameSuggestions &&
|
|
525
|
+
userNameSuggestions.length > 0 && (
|
|
526
|
+
<div data-fs-bp-create-user-userName-suggestions>
|
|
527
|
+
<ul>
|
|
528
|
+
{userNameSuggestions.map((suggestion) => (
|
|
529
|
+
<li key={suggestion}>
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={() =>
|
|
533
|
+
handleSelectUsernameSuggestion(suggestion)
|
|
534
|
+
}
|
|
535
|
+
>
|
|
536
|
+
{suggestion}
|
|
537
|
+
</button>
|
|
538
|
+
</li>
|
|
539
|
+
))}
|
|
540
|
+
</ul>
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<ErrorMessage
|
|
546
|
+
show={isUsernameTouched && !userName?.trim()}
|
|
547
|
+
message="Username is required"
|
|
548
|
+
/>
|
|
549
|
+
|
|
550
|
+
<div data-fs-bp-create-user-roles>
|
|
551
|
+
<span data-fs-bp-create-user-roles-label>Roles</span>
|
|
552
|
+
|
|
553
|
+
{rolesOptions &&
|
|
554
|
+
rolesOptions.map((role) => {
|
|
555
|
+
const id = `role-${role.roleName
|
|
556
|
+
.toLowerCase()
|
|
557
|
+
.replace(" ", "-")}`;
|
|
558
|
+
return (
|
|
559
|
+
<span
|
|
560
|
+
data-fs-bp-create-user-role-wrapper
|
|
561
|
+
key={role.roleName}
|
|
562
|
+
>
|
|
563
|
+
<input
|
|
564
|
+
type="checkbox"
|
|
565
|
+
key={role.roleName}
|
|
566
|
+
value={role.roleName}
|
|
567
|
+
id={id}
|
|
568
|
+
checked={roles?.includes(role.roleId)}
|
|
569
|
+
onChange={(event) => {
|
|
570
|
+
const newRoles = event.target.checked
|
|
571
|
+
? [...(roles ?? []), role.roleId]
|
|
572
|
+
: roles?.filter((r) => r !== role.roleId) ?? [];
|
|
573
|
+
updateField("roles", newRoles);
|
|
574
|
+
}}
|
|
575
|
+
/>
|
|
576
|
+
<label htmlFor={id}>{role.roleName}</label>
|
|
577
|
+
</span>
|
|
578
|
+
);
|
|
579
|
+
})}
|
|
580
|
+
|
|
581
|
+
<ErrorMessage
|
|
582
|
+
show={isTouched && roles.length === 0}
|
|
583
|
+
message="At least one role is required"
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
</>
|
|
587
|
+
)}
|
|
588
|
+
</BasicDrawer.Body>
|
|
589
|
+
<BasicDrawer.Footer>
|
|
590
|
+
<BasicDrawer.Button variant="ghost" onClick={close}>
|
|
591
|
+
Cancel
|
|
592
|
+
</BasicDrawer.Button>
|
|
593
|
+
<BasicDrawer.Button
|
|
594
|
+
variant="confirm"
|
|
595
|
+
disabled={!isConfirmButtonEnabled || hasRolesError}
|
|
596
|
+
onClick={handleConfirmClick}
|
|
597
|
+
isLoading={
|
|
598
|
+
isAddUserToOrgUnitLoading ||
|
|
599
|
+
isValidateUsernameLoading ||
|
|
600
|
+
isResetPasswordLoading
|
|
601
|
+
}
|
|
602
|
+
>
|
|
603
|
+
Add
|
|
604
|
+
</BasicDrawer.Button>
|
|
605
|
+
</BasicDrawer.Footer>
|
|
606
|
+
</>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
{drawerState === "CONFIRM_SHARED_EMAIL" && (
|
|
610
|
+
<>
|
|
611
|
+
<BasicDrawer.Body data-fs-bp-create-user-shared-email-body>
|
|
612
|
+
<div data-fs-bp-create-user-shared-email-warning>
|
|
613
|
+
<Icon name="Warning" height={48} width={48} />
|
|
614
|
+
<h3>This email is already registered</h3>
|
|
615
|
+
</div>
|
|
616
|
+
<p>
|
|
617
|
+
The email <strong>{sharedEmailData.email}</strong> is already
|
|
618
|
+
registered for another user (
|
|
619
|
+
<strong>{sharedEmailData.existingUserName}</strong>).
|
|
620
|
+
</p>
|
|
621
|
+
<p>
|
|
622
|
+
Are you sure you want to continue? If you proceed, a temporary
|
|
623
|
+
access code will be generated for the new user.
|
|
624
|
+
</p>
|
|
625
|
+
</BasicDrawer.Body>
|
|
626
|
+
<BasicDrawer.Footer>
|
|
627
|
+
<BasicDrawer.Button variant="ghost" onClick={backToCreateUser}>
|
|
628
|
+
Cancel
|
|
629
|
+
</BasicDrawer.Button>
|
|
630
|
+
<BasicDrawer.Button
|
|
631
|
+
variant="confirm"
|
|
632
|
+
onClick={handleConfirmSharedEmail}
|
|
633
|
+
isLoading={isAddUserToOrgUnitLoading}
|
|
634
|
+
>
|
|
635
|
+
Continue
|
|
636
|
+
</BasicDrawer.Button>
|
|
637
|
+
</BasicDrawer.Footer>
|
|
638
|
+
</>
|
|
639
|
+
)}
|
|
640
|
+
|
|
641
|
+
{drawerState === "ACCESS_TOKEN_DISPLAY" && (
|
|
642
|
+
<>
|
|
643
|
+
<BasicDrawer.Body data-fs-bp-create-user-access-token-body>
|
|
644
|
+
<div data-fs-bp-create-user-access-token-success>
|
|
645
|
+
<Icon name="CheckSuccess" height={60} width={60} />
|
|
646
|
+
|
|
647
|
+
<h3>Password reset successfully</h3>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<div data-fs-bp-create-user-access-token-description>
|
|
651
|
+
<p>
|
|
652
|
+
A temporary access code has been generated. <br />
|
|
653
|
+
Share it with the user, as they have no individual email
|
|
654
|
+
registered.
|
|
655
|
+
<br />
|
|
656
|
+
<br />
|
|
657
|
+
The code expires in 12 hours. Copy it before closing this
|
|
658
|
+
window. Once closed, access will be lost and a password reset
|
|
659
|
+
will be required.
|
|
660
|
+
</p>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<div data-fs-bp-create-user-access-token-display>
|
|
664
|
+
<div data-fs-bp-create-user-access-token-value>
|
|
665
|
+
<span>{accessToken}</span>
|
|
666
|
+
|
|
667
|
+
<IconButton
|
|
668
|
+
icon={
|
|
669
|
+
<Icon
|
|
670
|
+
name={hasTokenBeenCopied ? "Check" : "Copy"}
|
|
671
|
+
height={20}
|
|
672
|
+
width={20}
|
|
673
|
+
/>
|
|
674
|
+
}
|
|
675
|
+
size="small"
|
|
676
|
+
aria-label={hasTokenBeenCopied ? "Copied" : "Copy"}
|
|
677
|
+
onClick={handleCopyAccessToken}
|
|
678
|
+
data-fs-bp-create-user-copy-button
|
|
679
|
+
data-copied={hasTokenBeenCopied}
|
|
680
|
+
/>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
</BasicDrawer.Body>
|
|
684
|
+
<BasicDrawer.Footer>
|
|
685
|
+
<BasicDrawer.Button
|
|
686
|
+
variant="confirm"
|
|
687
|
+
onClick={handleCloseAccessTokenModal}
|
|
688
|
+
>
|
|
689
|
+
Done
|
|
690
|
+
</BasicDrawer.Button>
|
|
691
|
+
</BasicDrawer.Footer>
|
|
692
|
+
</>
|
|
693
|
+
)}
|
|
694
|
+
</BasicDrawer>
|
|
695
|
+
);
|
|
696
|
+
};
|