@firecms/user_management 3.0.1 → 3.1.0-canary.02232f4

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.
@@ -3,35 +3,40 @@ import * as Yup from "yup";
3
3
  import {
4
4
  Button,
5
5
  CheckIcon,
6
+ Chip,
7
+ CopyIcon,
6
8
  Dialog,
7
9
  DialogActions,
8
10
  DialogContent,
9
11
  DialogTitle,
12
+ IconButton,
10
13
  LoadingButton,
11
14
  MultiSelect,
12
15
  MultiSelectItem,
13
16
  TextField,
17
+ Tooltip,
14
18
  } from "@firecms/ui";
15
- import { FieldCaption, Role, useAuthController, User, useSnackbarController } from "@firecms/core";
19
+ import { FieldCaption, Role, useAuthController, User, useSnackbarController, useTranslation
20
+ } from "@firecms/core";
16
21
  import { Formex, useCreateFormex } from "@firecms/formex";
17
22
 
18
23
  import { areRolesEqual } from "../../utils";
19
24
  import { useUserManagement } from "../../hooks";
20
25
  import { RoleChip } from "../roles";
21
26
 
22
- export const UserYupSchema = Yup.object().shape({
23
- displayName: Yup.string().required("Required"),
24
- email: Yup.string().email().required("Required"),
27
+ export const getUserYupSchema = (t: any) => Yup.object().shape({
28
+ displayName: Yup.string().required(t("required")),
29
+ email: Yup.string().email().required(t("required")),
25
30
  roles: Yup.array().min(1)
26
31
  });
27
32
 
28
- function canUserBeEdited(loggedUser: User, user: User, users: User[], roles: Role[], prevUser?: User) {
33
+ function canUserBeEdited(loggedUser: User, user: User, users: User[], roles: Role[], t: any, prevUser?: User) {
29
34
  const admins = users.filter(u => u.roles?.map(r => r.id).includes("admin"));
30
35
  const loggedUserIsAdmin = loggedUser.roles?.map(r => r.id).includes("admin");
31
36
  const didRolesChange = !prevUser || !areRolesEqual(prevUser.roles ?? [], user.roles ?? []);
32
37
 
33
38
  if (didRolesChange && !loggedUserIsAdmin) {
34
- throw new Error("Only admins can change roles");
39
+ throw new Error(t("only_admins_change_roles"));
35
40
  }
36
41
 
37
42
  // was the admin role removed
@@ -39,7 +44,7 @@ function canUserBeEdited(loggedUser: User, user: User, users: User[], roles: Rol
39
44
 
40
45
  // avoid removing the last admin
41
46
  if (adminRoleRemoved && admins.length === 1) {
42
- throw new Error("There must be at least one admin");
47
+ throw new Error(t("must_be_at_least_one_admin"));
43
48
  }
44
49
  return true;
45
50
  }
@@ -53,7 +58,7 @@ export function UserDetailsForm({
53
58
  user?: User,
54
59
  handleClose: () => void
55
60
  }) {
56
-
61
+ const { t } = useTranslation();
57
62
  const snackbarController = useSnackbarController();
58
63
  const {
59
64
  user: loggedInUser
@@ -67,15 +72,15 @@ export function UserDetailsForm({
67
72
 
68
73
  const onUserUpdated = useCallback((savedUser: User): Promise<User> => {
69
74
  if (!loggedInUser) {
70
- throw new Error("Logged user not found");
75
+ throw new Error(t("logged_user_not_found"));
71
76
  }
72
77
  try {
73
- canUserBeEdited(loggedInUser, savedUser, users, roles, userProp);
78
+ canUserBeEdited(loggedInUser, savedUser, users, roles, t, userProp);
74
79
  return saveUser(savedUser);
75
80
  } catch (e: any) {
76
81
  return Promise.reject(e);
77
82
  }
78
- }, [roles, saveUser, userProp, users, loggedInUser]);
83
+ }, [roles, saveUser, userProp, users, loggedInUser, t]);
79
84
 
80
85
  const formex = useCreateFormex({
81
86
  initialValues: userProp ?? {
@@ -84,7 +89,7 @@ export function UserDetailsForm({
84
89
  roles: roles.filter(r => r.id === "editor")
85
90
  } as User,
86
91
  validation: (values) => {
87
- return UserYupSchema.validate(values, { abortEarly: false })
92
+ return getUserYupSchema(t).validate(values, { abortEarly: false })
88
93
  .then(() => {
89
94
  return {};
90
95
  }).catch((e) => {
@@ -142,7 +147,29 @@ export function UserDetailsForm({
142
147
  }}>
143
148
 
144
149
  <DialogTitle variant={"h4"} gutterBottom={false}>
145
- User
150
+ <div className="flex items-center justify-between">
151
+ <div>{t("user")}</div>
152
+ {!isNewUser && userProp?.uid && (
153
+ <div className="flex items-center gap-2">
154
+ <span className={"text-xs font-mono text-surface-accent-500 dark:text-surface-accent-400 font-normal"}>
155
+ {userProp.uid}
156
+ </span>
157
+ <Tooltip title={t("copy")}>
158
+ <IconButton
159
+ size={"smallest"}
160
+ onClick={() => {
161
+ navigator.clipboard.writeText(userProp.uid!);
162
+ snackbarController.open({
163
+ type: "success",
164
+ message: t("copied")
165
+ });
166
+ }}>
167
+ <CopyIcon size={"smallest"}/>
168
+ </IconButton>
169
+ </Tooltip>
170
+ </div>
171
+ )}
172
+ </div>
146
173
  </DialogTitle>
147
174
  <DialogContent className="h-full flex-grow">
148
175
 
@@ -156,10 +183,10 @@ export function UserDetailsForm({
156
183
  value={values.displayName ?? ""}
157
184
  onChange={handleChange}
158
185
  aria-describedby="name-helper-text"
159
- label="Name"
186
+ label={t("name")}
160
187
  />
161
188
  <FieldCaption>
162
- {submitCount > 0 && Boolean(errors.displayName) ? errors.displayName : "Name of this user"}
189
+ {submitCount > 0 && Boolean(errors.displayName) ? errors.displayName : t("name_of_this_user")}
163
190
  </FieldCaption>
164
191
  </div>
165
192
  <div className={"col-span-12"}>
@@ -170,16 +197,16 @@ export function UserDetailsForm({
170
197
  value={values.email ?? ""}
171
198
  onChange={handleChange}
172
199
  aria-describedby="email-helper-text"
173
- label="Email"
200
+ label={t("email")}
174
201
  />
175
202
  <FieldCaption>
176
- {submitCount > 0 && Boolean(errors.email) ? errors.email : "Email of this user"}
203
+ {submitCount > 0 && Boolean(errors.email) ? errors.email : t("email_of_this_user")}
177
204
  </FieldCaption>
178
205
  </div>
179
206
  <div className={"col-span-12"}>
180
207
  <MultiSelect
181
208
  className={"w-full"}
182
- label="Roles"
209
+ label={t("roles")}
183
210
  value={values.roles?.map(r => r.id) ?? []}
184
211
  onValueChange={(value: string[]) => setFieldValue("roles", value.map(id => roles.find(r => r.id === id) as Role))}
185
212
  // renderValue={(value: string) => {
@@ -205,21 +232,17 @@ export function UserDetailsForm({
205
232
  <DialogActions>
206
233
 
207
234
  <Button variant={"text"}
208
- color={"primary"}
209
235
  onClick={() => {
210
236
  handleClose();
211
- }}>
212
- Cancel
213
- </Button>
237
+ }}>{t("cancel")}</Button>
214
238
 
215
239
  <LoadingButton
216
240
  variant="filled"
217
- color="primary"
218
241
  type="submit"
219
242
  disabled={!dirty}
220
243
  loading={isSubmitting}
221
244
  >
222
- {isNewUser ? "Create user" : "Update"}
245
+ {isNewUser ? t("create_user") : t("update")}
223
246
  </LoadingButton>
224
247
  </DialogActions>
225
248
  </form>
@@ -8,9 +8,11 @@ import {
8
8
  ConfirmationDialog, Role,
9
9
  useAuthController,
10
10
  useCustomizationController, User,
11
- useSnackbarController
11
+ useSnackbarController,
12
+ useTranslation
12
13
  } from "@firecms/core";
13
14
  import {
15
+ Avatar,
14
16
  Button,
15
17
  CenteredView,
16
18
  DeleteIcon,
@@ -30,6 +32,7 @@ import { PersistedUser } from "../../types";
30
32
  export function UsersTable({ onUserClicked }: {
31
33
  onUserClicked: (user: User) => void;
32
34
  }) {
35
+ const { t } = useTranslation();
33
36
 
34
37
  const {
35
38
  users,
@@ -53,11 +56,12 @@ export function UsersTable({ onUserClicked }: {
53
56
  <Table className={"w-full"}>
54
57
 
55
58
  <TableHeader>
56
- <TableCell className="truncate w-16"></TableCell>
57
- <TableCell>Email</TableCell>
58
- <TableCell>Name</TableCell>
59
- <TableCell>Roles</TableCell>
60
- <TableCell>Created on</TableCell>
59
+ <TableCell className="w-12"></TableCell>
60
+ <TableCell>{t("email")}</TableCell>
61
+ <TableCell>{t("name")}</TableCell>
62
+ <TableCell>{t("roles")}</TableCell>
63
+ <TableCell>{t("created_on")}</TableCell>
64
+ <TableCell className="w-12"></TableCell>
61
65
  </TableHeader>
62
66
  <TableBody>
63
67
  {users && users.map((user) => {
@@ -73,32 +77,53 @@ export function UsersTable({ onUserClicked }: {
73
77
  onUserClicked(user);
74
78
  }}
75
79
  >
76
- <TableCell className={"w-10"}>
77
- <Tooltip
78
- asChild={true}
79
- title={"Delete this user"}>
80
- <IconButton
81
- size={"small"}
82
- onClick={(event) => {
83
- event.stopPropagation();
84
- return setUserToBeDeleted(user);
85
- }}>
86
- <DeleteIcon/>
87
- </IconButton>
88
- </Tooltip>
80
+ <TableCell className={"w-12"}>
81
+ <Avatar
82
+ src={user.photoURL ?? undefined}
83
+ outerClassName="w-8 h-8 min-w-8 min-h-8 p-0"
84
+ className="text-sm"
85
+ hover={false}
86
+ >
87
+ {user.displayName
88
+ ? user.displayName[0].toUpperCase()
89
+ : (user.email ? user.email[0].toUpperCase() : "U")}
90
+ </Avatar>
91
+ </TableCell>
92
+ <TableCell>
93
+ <div className="flex flex-col">
94
+ <div>{user.email}</div>
95
+ {user.uid && (
96
+ <div className="text-xs text-surface-accent-500 dark:text-surface-accent-400 font-mono mt-1">
97
+ {user.uid}
98
+ </div>
99
+ )}
100
+ </div>
89
101
  </TableCell>
90
- <TableCell>{user.email}</TableCell>
91
102
  <TableCell className={"font-medium align-left"}>{user.displayName}</TableCell>
92
103
  <TableCell className="align-left">
93
104
  {userRoles
94
105
  ? <div className="flex flex-wrap gap-2">
95
106
  {userRoles.map(userRole =>
96
- <RoleChip key={userRole?.id} role={userRole}/>
107
+ <RoleChip key={userRole?.id} role={userRole} />
97
108
  )}
98
109
  </div>
99
110
  : null}
100
111
  </TableCell>
101
112
  <TableCell>{formattedDate}</TableCell>
113
+ <TableCell className={"w-12"}>
114
+ <Tooltip
115
+ asChild={true}
116
+ title={t("delete_this_user")}>
117
+ <IconButton
118
+ size={"smallest"}
119
+ onClick={(event) => {
120
+ event.stopPropagation();
121
+ return setUserToBeDeleted(user);
122
+ }}>
123
+ <DeleteIcon size={"small"} />
124
+ </IconButton>
125
+ </Tooltip>
126
+ </TableCell>
102
127
  </TableRow>
103
128
  );
104
129
  })}
@@ -107,38 +132,37 @@ export function UsersTable({ onUserClicked }: {
107
132
  <TableCell colspan={6}>
108
133
  <CenteredView className={"flex flex-col gap-4 my-8 items-center"}>
109
134
  <Typography variant={"label"}>
110
- There are no users yet
135
+ {t("no_users_yet")}
111
136
  </Typography>
112
- <Button variant={"outlined"}
113
- onClick={() => {
114
- if (!authController.user?.uid) {
115
- throw Error("UsersTable, authController misconfiguration");
116
- }
117
- saveUser({
118
- uid: authController.user?.uid,
119
- email: authController.user?.email,
120
- displayName: authController.user?.displayName,
121
- photoURL: authController.user?.photoURL,
122
- providerId: authController.user?.providerId,
123
- isAnonymous: authController.user?.isAnonymous,
124
- roles: [{ id: "admin", name: "Admin" }],
125
- created_on: new Date()
137
+ <Button
138
+ onClick={() => {
139
+ if (!authController.user?.uid) {
140
+ throw Error("UsersTable, authController misconfiguration");
141
+ }
142
+ saveUser({
143
+ uid: authController.user?.uid,
144
+ email: authController.user?.email,
145
+ displayName: authController.user?.displayName,
146
+ photoURL: authController.user?.photoURL,
147
+ providerId: authController.user?.providerId,
148
+ isAnonymous: authController.user?.isAnonymous,
149
+ roles: [{ id: "admin", name: "Admin" }],
150
+ created_on: new Date()
151
+ })
152
+ .then(() => {
153
+ snackbarController.open({
154
+ type: "success",
155
+ message: "User added successfully"
156
+ })
126
157
  })
127
- .then(() => {
128
- snackbarController.open({
129
- type: "success",
130
- message: "User added successfully"
131
- })
158
+ .catch((error) => {
159
+ snackbarController.open({
160
+ type: "error",
161
+ message: "Error adding user: " + error.message,
132
162
  })
133
- .catch((error) => {
134
- snackbarController.open({
135
- type: "error",
136
- message: "Error adding user: " + error.message,
137
- })
138
- });
139
- }}>
140
-
141
- Add the logged user as an admin
163
+ });
164
+ }}>
165
+ {t("add_logged_user_as_admin")}
142
166
  </Button>
143
167
  </CenteredView>
144
168
  </TableCell>
@@ -171,7 +195,7 @@ export function UsersTable({ onUserClicked }: {
171
195
  onCancel={() => {
172
196
  setUserToBeDeleted(undefined);
173
197
  }}
174
- title={<>Delete?</>}
175
- body={<>Are you sure you want to delete this user?</>}/>
198
+ title={<>{t("delete_confirmation_title")}</>}
199
+ body={<>{t("delete_user_confirmation")}</>} />
176
200
  </div>);
177
201
  }
@@ -4,9 +4,12 @@ import { UsersTable } from "./UsersTable";
4
4
  import { UserDetailsForm } from "./UserDetailsForm";
5
5
  import React, { useCallback, useState } from "react";
6
6
  import { useUserManagement } from "../../hooks/useUserManagement";
7
- import { User } from "@firecms/core";
7
+ import { User, useTranslation
8
+ } from "@firecms/core";
8
9
 
9
10
  export const UsersView = function UsersView({ children }: { children?: React.ReactNode }) {
11
+ const { t } = useTranslation();
12
+
10
13
 
11
14
  const [dialogOpen, setDialogOpen] = useState<boolean>(false);
12
15
  const [selectedUser, setSelectedUser] = useState<User | undefined>();
@@ -39,15 +42,11 @@ export const UsersView = function UsersView({ children }: { children?: React.Rea
39
42
  className="flex items-center mt-12">
40
43
  <Typography gutterBottom variant="h4"
41
44
  className="flex-grow"
42
- component="h4">
43
- Users
44
- </Typography>
45
+ component="h4">{t("users")}</Typography>
45
46
  <Button
46
47
  size={"large"}
47
48
  startIcon={<AddIcon/>}
48
- onClick={handleAddUser}>
49
- Add user
50
- </Button>
49
+ onClick={handleAddUser}>{t("add_user")}</Button>
51
50
  </div>
52
51
 
53
52
  <UsersTable onUserClicked={onUserClicked}/>
@@ -298,12 +298,17 @@ export function useBuildUserManagement<CONTROLLER extends AuthController<any> =
298
298
 
299
299
  const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
300
300
  if (mgmtUser) {
301
- // check if the uid is updated in the user management system
302
- if (mgmtUser.uid !== user.uid) {
303
- console.warn("User uid has changed, updating user in user management system");
301
+ // check if the uid or photoURL needs to be updated in the user management system
302
+ const needsUidUpdate = mgmtUser.uid !== user.uid;
303
+ const needsPhotoUpdate = user.photoURL && mgmtUser.photoURL !== user.photoURL;
304
+
305
+ if (needsUidUpdate || needsPhotoUpdate) {
306
+ const updateReason = needsUidUpdate ? "uid" : "photoURL";
307
+ console.debug(`User ${updateReason} has changed, updating user in user management system`);
304
308
  saveUser({
305
309
  ...mgmtUser,
306
- uid: user.uid
310
+ uid: user.uid,
311
+ ...(needsPhotoUpdate ? { photoURL: user.photoURL } : {})
307
312
  }).then(() => {
308
313
  console.debug("User updated in user management system", mgmtUser);
309
314
  }).catch(e => {
@@ -322,7 +327,10 @@ export function useBuildUserManagement<CONTROLLER extends AuthController<any> =
322
327
 
323
328
  const userRoleIds = userRoles?.map(r => r.id);
324
329
  useEffect(() => {
325
- console.debug("Setting user roles", { userRoles, roles });
330
+ console.debug("Setting user roles", {
331
+ userRoles,
332
+ roles
333
+ });
326
334
  authController.setUserRoles?.(userRoles ?? []);
327
335
  }, [userRoleIds]);
328
336
 
@@ -1,4 +1,4 @@
1
- import { FireCMSPlugin, useAuthController, User, useSnackbarController } from "@firecms/core";
1
+ import { FireCMSPlugin, useAuthController, User, useSnackbarController, useTranslation } from "@firecms/core";
2
2
  import { UserManagementProvider } from "./UserManagementProvider";
3
3
  import { UserManagement } from "./types";
4
4
  import { AddIcon, Button, Paper, Typography } from "@firecms/ui";
@@ -46,22 +46,23 @@ export function IntroWidget({
46
46
 
47
47
  const authController = useAuthController();
48
48
  const snackbarController = useSnackbarController();
49
+ const { t } = useTranslation();
49
50
 
50
51
  const buttonLabel = noUsers && noRoles
51
- ? "Create default roles and add current user as admin"
52
+ ? t("create_default_roles_and_add_admin")
52
53
  : noUsers
53
- ? "Add current user as admin"
54
- : noRoles ? "Create default roles" : undefined;
54
+ ? t("add_current_user_as_admin")
55
+ : noRoles ? t("create_default_roles") : undefined;
55
56
 
56
57
  return (
57
58
  <Paper
58
59
  className={"my-4 flex flex-col px-4 py-6 bg-white dark:bg-surface-accent-800 gap-2"}>
59
- <Typography variant={"subtitle2"} className={"uppercase"}>Create your users and roles</Typography>
60
+ <Typography variant={"subtitle2"} className={"uppercase"}>{t("create_your_users_and_roles")}</Typography>
60
61
  <Typography>
61
- You have no users or roles defined. You can create default roles and add the current user as admin.
62
+ {t("no_users_or_roles_defined")}
62
63
  </Typography>
63
64
  <Button
64
- variant={"outlined"}
65
+
65
66
  onClick={() => {
66
67
  if (!authController.user?.uid) {
67
68
  throw Error("UsersTable, authController misconfiguration");