@plone/volto 18.29.0 → 18.29.1

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.
@@ -33,13 +33,13 @@ import find from 'lodash/find';
33
33
  import map from 'lodash/map';
34
34
  import pull from 'lodash/pull';
35
35
  import difference from 'lodash/difference';
36
- import PropTypes from 'prop-types';
37
- import React, { Component } from 'react';
38
- import { FormattedMessage, injectIntl } from 'react-intl';
36
+
37
+ import { useState, useEffect, useCallback } from 'react';
38
+ import { FormattedMessage, useIntl } from 'react-intl';
39
39
  import { createPortal } from 'react-dom';
40
- import { connect } from 'react-redux';
40
+ import { useSelector, useDispatch } from 'react-redux';
41
+ import { useLocation } from 'react-router-dom';
41
42
  import { toast } from 'react-toastify';
42
- import { bindActionCreators, compose } from 'redux';
43
43
  import {
44
44
  Confirm,
45
45
  Container,
@@ -53,151 +53,132 @@ import {
53
53
  } from 'semantic-ui-react';
54
54
 
55
55
  /**
56
- * UsersControlpanel class.
57
- * @class UsersControlpanel
58
- * @extends Component
56
+ * UsersControlpanel functional component.
57
+ * @function UsersControlpanel
59
58
  */
60
- class UsersControlpanel extends Component {
61
- /**
62
- * Property types.
63
- * @property {Object} propTypes Property types.
64
- * @static
65
- */
66
- static propTypes = {
67
- listRoles: PropTypes.func.isRequired,
68
- listUsers: PropTypes.func.isRequired,
69
- updateUser: PropTypes.func,
70
- listGroups: PropTypes.func.isRequired,
71
- pathname: PropTypes.string.isRequired,
72
- roles: PropTypes.arrayOf(
73
- PropTypes.shape({
74
- '@id': PropTypes.string,
75
- '@type': PropTypes.string,
76
- id: PropTypes.string,
77
- }),
78
- ).isRequired,
79
- users: PropTypes.arrayOf(
80
- PropTypes.shape({
81
- username: PropTypes.string,
82
- fullname: PropTypes.string,
83
- roles: PropTypes.arrayOf(PropTypes.string),
84
- }),
85
- ).isRequired,
86
- user: PropTypes.shape({
87
- '@id': PropTypes.string,
88
- id: PropTypes.string,
89
- description: PropTypes.string,
90
- email: PropTypes.string,
91
- fullname: PropTypes.string,
92
- groups: PropTypes.object,
93
- location: PropTypes.string,
94
- portrait: PropTypes.string,
95
- home_page: PropTypes.string,
96
- roles: PropTypes.arrayOf(PropTypes.string),
97
- username: PropTypes.string,
98
- }).isRequired,
99
- };
59
+ const UsersControlpanel = () => {
60
+ const intl = useIntl();
61
+ const dispatch = useDispatch();
100
62
 
101
- /**
102
- * Constructor
103
- * @method constructor
104
- * @param {Object} props Component properties
105
- * @constructs Sharing
106
- */
107
- constructor(props) {
108
- super(props);
109
- this.onChangeSearch = this.onChangeSearch.bind(this);
110
- this.onSearch = this.onSearch.bind(this);
111
- this.delete = this.delete.bind(this);
112
-
113
- this.onDeleteOk = this.onDeleteOk.bind(this);
114
- this.onDeleteCancel = this.onDeleteCancel.bind(this);
115
- this.onAddUserSubmit = this.onAddUserSubmit.bind(this);
116
- this.onAddUserError = this.onAddUserError.bind(this);
117
- this.onAddUserSuccess = this.onAddUserSuccess.bind(this);
118
- this.updateUserRole = this.updateUserRole.bind(this);
119
- this.state = {
120
- search: '',
121
- isLoading: false,
122
- showAddUser: false,
123
- showAddUserErrorConfirm: false,
124
- addUserError: '',
125
- showDelete: false,
126
- userToDelete: undefined,
127
- entries: [],
128
- isClient: false,
129
- currentPage: 0,
130
- pageSize: 10,
131
- loginUsingEmail: false,
132
- };
133
- }
63
+ // Redux state selectors
64
+ const roles = useSelector((state) => state.roles.roles);
65
+ const users = useSelector((state) => state.users.users);
66
+ const user = useSelector((state) => state.users.user);
67
+ const userId = useSelector((state) =>
68
+ state.userSession.token ? jwtDecode(state.userSession.token).sub : '',
69
+ );
70
+ const groups = useSelector((state) => state.groups.groups);
71
+ const many_users = useSelector(
72
+ (state) => state.controlpanels?.controlpanel?.data?.many_users,
73
+ );
134
74
 
135
- fetchData = async () => {
136
- await this.props.getControlpanel('usergroup');
137
- await this.props.listRoles();
138
- if (!this.props.many_users) {
139
- this.props.listGroups();
140
- await this.props.listUsers();
141
- this.setState({
142
- entries: this.props.users,
143
- });
144
- }
145
- await this.props.getUserSchema();
146
- await this.props.getUser(this.props.userId);
147
- };
75
+ const location = useLocation();
76
+ const pathname = location.pathname;
77
+ const deleteRequest = useSelector((state) => state.users.delete);
78
+ const createRequest = useSelector((state) => state.users.create);
79
+ const loadRolesRequest = useSelector((state) => state.roles);
80
+ const inheritedRole = useSelector(
81
+ (state) => state.authRole.authenticatedRole,
82
+ );
83
+ const userschema = useSelector((state) => state.userschema);
84
+ const controlPanelData = useSelector(
85
+ (state) => state.controlpanels?.controlpanel,
86
+ );
148
87
 
149
- // Because username field needs to be disabled if email login is enabled!
150
- checkLoginUsingEmailStatus = async () => {
151
- await this.props.getControlpanel('security');
152
- this.setState({
153
- loginUsingEmail: this.props.controlPanelData?.data.use_email_as_login,
154
- });
155
- };
88
+ // Action creators
89
+ const listRolesAction = useCallback(() => dispatch(listRoles()), [dispatch]);
90
+ const listUsersAction = useCallback(
91
+ (params) => dispatch(listUsers(params)),
92
+ [dispatch],
93
+ );
94
+ const listGroupsAction = useCallback(
95
+ () => dispatch(listGroups()),
96
+ [dispatch],
97
+ );
98
+ const getControlpanelAction = useCallback(
99
+ (panel) => dispatch(getControlpanel(panel)),
100
+ [dispatch],
101
+ );
102
+ const deleteUserAction = useCallback(
103
+ (userId) => dispatch(deleteUser(userId)),
104
+ [dispatch],
105
+ );
106
+ const updateUserAction = useCallback(
107
+ (userId, data) => dispatch(updateUser(userId, data)),
108
+ [dispatch],
109
+ );
110
+ const updateGroupAction = useCallback(
111
+ (groupId, data) => dispatch(updateGroup(groupId, data)),
112
+ [dispatch],
113
+ );
114
+ const getUserSchemaAction = useCallback(
115
+ () => dispatch(getUserSchema()),
116
+ [dispatch],
117
+ );
118
+ const getUserAction = useCallback(
119
+ (userId) => dispatch(getUser(userId)),
120
+ [dispatch],
121
+ );
122
+
123
+ const [search, setSearch] = useState('');
124
+ const [isLoading, setIsLoading] = useState(false);
125
+ const [showAddUser, setShowAddUser] = useState(false);
126
+ const [addUserError, setAddUserError] = useState('');
127
+ const [showDelete, setShowDelete] = useState(false);
128
+ const [userToDelete, setUserToDelete] = useState(undefined);
129
+ const [entries, setEntries] = useState([]);
130
+ const [isClient, setIsClient] = useState(false);
131
+ const [currentPage, setCurrentPage] = useState(0);
132
+ const [pageSize] = useState(10);
133
+ // eslint-disable-next-line no-unused-vars
134
+ const [loginUsingEmail, setLoginUsingEmail] = useState(false); // Reserved for future use to disable username field when email login is enabled
135
+ const [error, setError] = useState(null);
136
+
137
+ const fetchData = useCallback(async () => {
138
+ await getControlpanelAction('usergroup');
139
+ await listRolesAction();
140
+ if (!many_users) {
141
+ listGroupsAction();
142
+ await listUsersAction();
143
+ setEntries(users);
144
+ }
145
+ await getUserSchemaAction();
146
+ await getUserAction(userId);
147
+ }, [
148
+ getControlpanelAction,
149
+ listRolesAction,
150
+ many_users,
151
+ listGroupsAction,
152
+ listUsersAction,
153
+ users,
154
+ getUserSchemaAction,
155
+ getUserAction,
156
+ userId,
157
+ ]);
156
158
 
157
159
  /**
158
- * Component did mount
159
- * @method componentDidMount
160
+ * Check login using email status from security control panel
161
+ * @method checkLoginUsingEmailStatus
160
162
  * @returns {undefined}
161
163
  */
162
- componentDidMount() {
163
- this.setState({
164
- isClient: true,
165
- });
166
- this.fetchData();
167
- this.checkLoginUsingEmailStatus();
168
- }
169
-
170
- UNSAFE_componentWillReceiveProps(nextProps) {
171
- if (
172
- (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) ||
173
- (this.props.createRequest.loading && nextProps.createRequest.loaded)
174
- ) {
175
- this.props.listUsers({
176
- search: this.state.search,
177
- });
178
- }
179
- if (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) {
180
- this.onDeleteUserSuccess();
181
- }
182
- if (this.props.createRequest.loading && nextProps.createRequest.loaded) {
183
- this.onAddUserSuccess();
184
- }
185
- if (this.props.createRequest.loading && nextProps.createRequest.error) {
186
- this.onAddUserError(nextProps.createRequest.error);
187
- }
188
- if (
189
- this.props.loadRolesRequest.loading &&
190
- nextProps.loadRolesRequest.error
191
- ) {
192
- this.setState({
193
- error: nextProps.loadRolesRequest.error,
194
- });
164
+ const checkLoginUsingEmailStatus = useCallback(async () => {
165
+ try {
166
+ await getControlpanelAction('security');
167
+ if (controlPanelData?.data?.use_email_as_login) {
168
+ setLoginUsingEmail(controlPanelData.data.use_email_as_login);
169
+ }
170
+ } catch (error) {
171
+ // eslint-disable-next-line no-console
172
+ console.error('Error fetching security control panel', error);
195
173
  }
196
- }
174
+ }, [getControlpanelAction, controlPanelData]);
197
175
 
198
- getUserFromProps(value) {
199
- return find(this.props.users, ['@id', value]);
200
- }
176
+ const getUserFromProps = useCallback(
177
+ (value) => {
178
+ return find(users, ['@id', value]);
179
+ },
180
+ [users],
181
+ );
201
182
 
202
183
  /**
203
184
  * Search handler
@@ -205,22 +186,24 @@ class UsersControlpanel extends Component {
205
186
  * @param {object} event Event object.
206
187
  * @returns {undefined}
207
188
  */
208
- onSearch(event) {
209
- event.preventDefault();
210
- this.setState({ isLoading: true });
211
- this.props
212
- .listUsers({
213
- search: this.state.search,
214
- })
215
- .then(() => {
216
- this.setState({ isLoading: false });
189
+ const onSearch = useCallback(
190
+ (event) => {
191
+ event.preventDefault();
192
+ setIsLoading(true);
193
+ listUsersAction({
194
+ search: search,
217
195
  })
218
- .catch((error) => {
219
- this.setState({ isLoading: false });
220
- // eslint-disable-next-line no-console
221
- console.error('Error searching users', error);
222
- });
223
- }
196
+ .then(() => {
197
+ setIsLoading(false);
198
+ })
199
+ .catch((error) => {
200
+ setIsLoading(false);
201
+ // eslint-disable-next-line no-console
202
+ console.error('Error searching users', error);
203
+ });
204
+ },
205
+ [listUsersAction, search],
206
+ );
224
207
 
225
208
  /**
226
209
  * On change search handler
@@ -228,67 +211,94 @@ class UsersControlpanel extends Component {
228
211
  * @param {object} event Event object.
229
212
  * @returns {undefined}
230
213
  */
231
- onChangeSearch(event) {
232
- this.setState({
233
- search: event.target.value,
234
- });
235
- }
214
+ const onChangeSearch = (event) => {
215
+ setSearch(event.target.value);
216
+ };
236
217
 
237
218
  /**
238
- * Delete a user
239
- * @method delete
219
+ * Handle delete user click
220
+ * @method handleDeleteUser
240
221
  * @param {object} event Event object.
241
222
  * @param {string} value username.
242
223
  * @returns {undefined}
243
224
  */
244
- delete(event, { value }) {
245
- if (value) {
246
- this.setState({
247
- showDelete: true,
248
- userToDelete: this.getUserFromProps(value),
249
- });
250
- }
251
- }
225
+ const handleDeleteUser = useCallback(
226
+ (event, data) => {
227
+ // Handle both formats: direct value from event target or object with value
228
+ const value =
229
+ data?.value || event?.target?.value || event?.currentTarget?.value;
230
+ if (value) {
231
+ setShowDelete(true);
232
+ setUserToDelete(getUserFromProps(value));
233
+ }
234
+ },
235
+ [getUserFromProps],
236
+ );
252
237
 
253
238
  /**
254
239
  * On delete ok
255
240
  * @method onDeleteOk
256
241
  * @returns {undefined}
257
242
  */
258
- onDeleteOk() {
259
- if (this.state.userToDelete) {
260
- this.props.deleteUser(this.state.userToDelete.id);
243
+ const onDeleteOk = useCallback(() => {
244
+ if (userToDelete) {
245
+ const deleteAction = deleteUserAction(userToDelete.id);
246
+ if (deleteAction && typeof deleteAction.then === 'function') {
247
+ deleteAction
248
+ .then(() => {
249
+ // Handle success
250
+ setUserToDelete(undefined);
251
+ setShowDelete(false);
252
+
253
+ // Refresh users list
254
+ listUsersAction({ search: search });
255
+
256
+ // Show success message
257
+ toast.success(
258
+ <Toast
259
+ success
260
+ title={intl.formatMessage(messages.success)}
261
+ content={intl.formatMessage(messages.userDeleted)}
262
+ />,
263
+ );
264
+ })
265
+ .catch((error) => {
266
+ // Handle error
267
+ // eslint-disable-next-line no-console
268
+ console.error('Error deleting user', error);
269
+ });
270
+ }
261
271
  }
262
- }
272
+ }, [userToDelete, deleteUserAction, listUsersAction, search, intl]);
263
273
 
264
274
  /**
265
275
  * On delete cancel
266
276
  * @method onDeleteCancel
267
277
  * @returns {undefined}
268
278
  */
269
- onDeleteCancel() {
270
- this.setState({
271
- showDelete: false,
272
- itemsToDelete: [],
273
- userToDelete: undefined,
274
- });
275
- }
279
+ const onDeleteCancel = () => {
280
+ setShowDelete(false);
281
+ setUserToDelete(undefined);
282
+ };
276
283
 
277
284
  /**
278
285
  *@param {object} user
279
286
  *@returns {undefined}
280
287
  *@memberof UsersControlpanel
281
288
  */
282
- addUserToGroup = (user) => {
283
- const { groups, username } = user;
284
- groups.forEach((group) => {
285
- this.props.updateGroup(group, {
286
- users: {
287
- [username]: true,
288
- },
289
+ const addUserToGroup = useCallback(
290
+ (user) => {
291
+ const { groups: userGroups, username } = user;
292
+ userGroups.forEach((group) => {
293
+ updateGroupAction(group, {
294
+ users: {
295
+ [username]: true,
296
+ },
297
+ });
289
298
  });
290
- });
291
- };
299
+ },
300
+ [updateGroupAction],
301
+ );
292
302
 
293
303
  /**
294
304
  * Callback to be called by the ModalForm when the form is submitted.
@@ -297,119 +307,112 @@ class UsersControlpanel extends Component {
297
307
  * @param {func} callback to set new form data in the ModalForm
298
308
  * @returns {undefined}
299
309
  */
300
- onAddUserSubmit(data, callback) {
301
- const { groups, sendPasswordReset, password } = data;
302
- if (
303
- sendPasswordReset !== undefined &&
304
- sendPasswordReset === true &&
305
- password !== undefined
306
- ) {
307
- toast.error(
308
- <Toast
309
- error
310
- title={this.props.intl.formatMessage(messages.error)}
311
- content={this.props.intl.formatMessage(
312
- messages.addUserFormPasswordAndSendPasswordTogetherNotAllowed,
313
- )}
314
- />,
315
- );
316
- } else {
317
- if (groups && groups.length > 0) this.addUserToGroup(data);
318
- this.props.createUser(data, sendPasswordReset);
319
- this.setState({
320
- addUserSetFormDataCallback: callback,
321
- });
322
- }
323
- }
310
+ const onAddUserSubmit = useCallback(
311
+ (data, callback) => {
312
+ const { groups: userGroups, sendPasswordReset, password } = data;
313
+ if (
314
+ sendPasswordReset !== undefined &&
315
+ sendPasswordReset === true &&
316
+ password !== undefined
317
+ ) {
318
+ toast.error(
319
+ <Toast
320
+ error
321
+ title={intl.formatMessage(messages.error)}
322
+ content={intl.formatMessage(
323
+ messages.addUserFormPasswordAndSendPasswordTogetherNotAllowed,
324
+ )}
325
+ />,
326
+ );
327
+ } else {
328
+ if (userGroups && userGroups.length > 0) addUserToGroup(data);
324
329
 
325
- /**
326
- * Handle Success after createUser()
327
- *
328
- * @returns {undefined}
329
- */
330
- onAddUserSuccess() {
331
- this.state.addUserSetFormDataCallback({});
332
- this.setState({
333
- showAddUser: false,
334
- addUserError: undefined,
335
- addUserSetFormDataCallback: undefined,
336
- });
337
- toast.success(
338
- <Toast
339
- success
340
- title={this.props.intl.formatMessage(messages.success)}
341
- content={this.props.intl.formatMessage(messages.userCreated)}
342
- />,
343
- );
344
- }
330
+ const createUserAction = createUser(data, sendPasswordReset);
331
+ dispatch(createUserAction)
332
+ .then(() => {
333
+ // Handle success
334
+ if (callback) {
335
+ callback({});
336
+ }
337
+ setShowAddUser(false);
338
+ setAddUserError(undefined);
339
+
340
+ // Refresh users list
341
+ listUsersAction({ search: search });
342
+
343
+ // Show success message
344
+ toast.success(
345
+ <Toast
346
+ success
347
+ title={intl.formatMessage(messages.success)}
348
+ content={intl.formatMessage(messages.userCreated)}
349
+ />,
350
+ );
351
+ })
352
+ .catch((error) => {
353
+ // Handle error
354
+ setAddUserError(
355
+ error.response?.body?.error?.message || 'Error creating user',
356
+ );
357
+ });
358
+ }
359
+ },
360
+ [intl, addUserToGroup, dispatch, search, listUsersAction],
361
+ );
345
362
 
346
363
  /**
347
- * Handle Success after deleteUser()
348
- *
349
- * @returns {undefined}
350
- */
351
- onDeleteUserSuccess() {
352
- this.setState({
353
- userToDelete: undefined,
354
- showDelete: false,
355
- });
356
- toast.success(
357
- <Toast
358
- success
359
- title={this.props.intl.formatMessage(messages.success)}
360
- content={this.props.intl.formatMessage(messages.userDeleted)}
361
- />,
362
- );
363
- }
364
- /**
365
- *
366
- *
367
- * @param {*} data
368
- * @param {*} callback
369
- * @memberof UsersControlpanel
364
+ * Update user role
365
+ * @param {*} name
366
+ * @param {*} value
370
367
  */
371
- updateUserRole(name, value) {
372
- this.setState({
373
- entries: map(this.state.entries, (entry) => ({
374
- ...entry,
375
- roles:
376
- entry.id === name && !entry.roles.includes(value)
377
- ? [...entry.roles, value]
378
- : entry.id !== name
379
- ? entry.roles
380
- : pull(entry.roles, value),
381
- })),
382
- });
383
- }
368
+ const updateUserRole = useCallback(
369
+ (name, value) => {
370
+ setEntries(
371
+ map(entries, (entry) => ({
372
+ ...entry,
373
+ roles:
374
+ entry.id === name && !entry.roles.includes(value)
375
+ ? [...entry.roles, value]
376
+ : entry.id !== name
377
+ ? entry.roles
378
+ : pull(entry.roles, value),
379
+ })),
380
+ );
381
+ },
382
+ [entries],
383
+ );
384
+
384
385
  /**
385
- *
386
+ * Update user role submit
386
387
  * @param {*} event
387
- * @memberof UsersControlpanel
388
388
  */
389
- updateUserRoleSubmit = (e) => {
390
- e.stopPropagation();
389
+ const updateUserRoleSubmit = useCallback(
390
+ (e) => {
391
+ e.stopPropagation();
391
392
 
392
- const roles = this.props.roles.map((item) => item.id);
393
- this.state.entries.forEach((item) => {
394
- const userData = { roles: {} };
395
- const removedRoles = difference(roles, item.roles);
393
+ const roleIds = roles.map((item) => item.id);
394
+ entries.forEach((item) => {
395
+ const userData = { roles: {} };
396
+ const removedRoles = difference(roleIds, item.roles);
396
397
 
397
- removedRoles.forEach((role) => {
398
- userData.roles[role] = false;
399
- });
400
- item.roles.forEach((role) => {
401
- userData.roles[role] = true;
398
+ removedRoles.forEach((role) => {
399
+ userData.roles[role] = false;
400
+ });
401
+ item.roles.forEach((role) => {
402
+ userData.roles[role] = true;
403
+ });
404
+ updateUserAction(item.id, userData);
402
405
  });
403
- this.props.updateUser(item.id, userData);
404
- });
405
- toast.success(
406
- <Toast
407
- success
408
- title={this.props.intl.formatMessage(messages.success)}
409
- content={this.props.intl.formatMessage(messages.updateRoles)}
410
- />,
411
- );
412
- };
406
+ toast.success(
407
+ <Toast
408
+ success
409
+ title={intl.formatMessage(messages.success)}
410
+ content={intl.formatMessage(messages.updateRoles)}
411
+ />,
412
+ );
413
+ },
414
+ [roles, entries, updateUserAction, intl],
415
+ );
413
416
 
414
417
  /**
415
418
  * Handle Errors after createUser()
@@ -417,11 +420,9 @@ class UsersControlpanel extends Component {
417
420
  * @param {object} error object. Requires the property .message
418
421
  * @returns {undefined}
419
422
  */
420
- onAddUserError(error) {
421
- this.setState({
422
- addUserError: error.response.body.error.message,
423
- });
424
- }
423
+ const onAddUserError = useCallback((error) => {
424
+ setAddUserError(error.response.body.error.message);
425
+ }, []);
425
426
 
426
427
  /**
427
428
  * On change page
@@ -430,359 +431,303 @@ class UsersControlpanel extends Component {
430
431
  * @param {string} value Page value.
431
432
  * @returns {undefined}
432
433
  */
433
- onChangePage = (event, { value }) => {
434
- this.setState({
435
- currentPage: value,
436
- });
434
+ const onChangePage = (event, { value }) => {
435
+ setCurrentPage(value);
437
436
  };
438
437
 
439
- componentDidUpdate(prevProps, prevState) {
440
- if (this.props.users !== prevProps.users) {
441
- this.setState({
442
- entries: this.props.users,
443
- });
444
- }
445
- }
446
-
447
438
  /**
448
439
  * Filters the roles a user can assign when adding a user.
449
440
  * @method canAssignAdd
450
441
  * @returns {arry}
451
442
  */
452
- canAssignAdd(isManager) {
453
- if (isManager) return this.props.roles;
454
- return this.props.user?.roles
455
- ? this.props.roles.filter((role) =>
456
- this.props.user.roles.includes(role.id),
457
- )
458
- : [];
459
- }
443
+ const canAssignAdd = useCallback(
444
+ (isManager) => {
445
+ if (isManager) return roles;
446
+ return user?.roles
447
+ ? roles.filter((role) => user.roles.includes(role.id))
448
+ : [];
449
+ },
450
+ [roles, user],
451
+ );
460
452
 
461
- /**
462
- * Render method.
463
- * @method render
464
- * @returns {string} Markup for the component.
465
- */
466
- render() {
467
- if (this.state.error) {
468
- return <Error error={this.state.error} />;
453
+ useEffect(() => {
454
+ setIsClient(true);
455
+ fetchData();
456
+ checkLoginUsingEmailStatus();
457
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
458
+
459
+ useEffect(() => {
460
+ setEntries(users);
461
+ }, [users]);
462
+
463
+ useEffect(() => {
464
+ if (createRequest?.error && !createRequest?.loading) {
465
+ onAddUserError(createRequest.error);
469
466
  }
470
- /*let fullnameToDelete = this.state.userToDelete
471
- ? this.state.userToDelete.fullname
472
- : '';*/
473
- let usernameToDelete = this.state.userToDelete
474
- ? this.state.userToDelete.username
475
- : '';
476
- // Copy the userschema using JSON serialization/deserialization
477
- // this is really ugly, but if we don't do this the original value
478
- // of the userschema is changed and it is used like that through
479
- // the lifecycle of the application
480
- let adduserschema = {};
481
- let isUserManager = false;
482
- if (this.props?.userschema?.loaded) {
483
- isUserManager = isManager(this.props.user);
484
- adduserschema = JSON.parse(
485
- JSON.stringify(this.props?.userschema?.userschema),
486
- );
487
- adduserschema.properties['username'] = {
488
- title: this.props.intl.formatMessage(messages.addUserFormUsernameTitle),
489
- type: 'string',
490
- description: this.props.intl.formatMessage(
491
- messages.addUserFormUsernameDescription,
492
- ),
493
- };
494
- adduserschema.properties['password'] = {
495
- title: this.props.intl.formatMessage(messages.addUserFormPasswordTitle),
496
- type: 'password',
497
- description: this.props.intl.formatMessage(
498
- messages.addUserFormPasswordDescription,
499
- ),
500
- widget: 'password',
501
- };
502
- adduserschema.properties['sendPasswordReset'] = {
503
- title: this.props.intl.formatMessage(
504
- messages.addUserFormSendPasswordResetTitle,
505
- ),
506
- type: 'boolean',
507
- };
508
- adduserschema.properties['roles'] = {
509
- title: this.props.intl.formatMessage(messages.addUserFormRolesTitle),
510
- type: 'array',
511
- choices: this.canAssignAdd(isUserManager).map((role) => [
512
- role.id,
513
- role.title,
514
- ]),
515
- noValueOption: false,
516
- };
517
- adduserschema.properties['groups'] = {
518
- title: this.props.intl.formatMessage(messages.addUserGroupNameTitle),
519
- type: 'array',
520
- choices: this.props.groups
521
- .filter((group) => canAssignGroup(isUserManager, group))
522
- .map((group) => [group.id, group.id]),
523
- noValueOption: false,
524
- };
525
- if (
526
- adduserschema.fieldsets &&
527
- adduserschema.fieldsets.length > 0 &&
528
- !adduserschema.fieldsets[0]['fields'].includes('username')
529
- ) {
530
- adduserschema.fieldsets[0]['fields'] = adduserschema.fieldsets[0][
531
- 'fields'
532
- ].concat([
467
+ }, [createRequest?.error, createRequest?.loading, onAddUserError]);
468
+
469
+ useEffect(() => {
470
+ if (loadRolesRequest?.error && !loadRolesRequest?.loading) {
471
+ setError(loadRolesRequest.error);
472
+ }
473
+ }, [loadRolesRequest?.error, loadRolesRequest?.loading]);
474
+
475
+ if (error) {
476
+ return <Error error={error} />;
477
+ }
478
+
479
+ const usernameToDelete = userToDelete ? userToDelete.username : '';
480
+
481
+ // Copy the userschema using JSON serialization/deserialization
482
+ // this is really ugly, but if we don't do this the original value
483
+ // of the userschema is changed and it is used like that through
484
+ // the lifecycle of the application
485
+ let adduserschema = {};
486
+ let isUserManager = false;
487
+ if (userschema?.loaded) {
488
+ isUserManager = isManager(user);
489
+ adduserschema = JSON.parse(JSON.stringify(userschema?.userschema));
490
+
491
+ // Add custom form fields to the schema
492
+ adduserschema.properties.username = {
493
+ title: intl.formatMessage(messages.addUserFormUsernameTitle),
494
+ type: 'string',
495
+ description: intl.formatMessage(messages.addUserFormUsernameDescription),
496
+ };
497
+
498
+ adduserschema.properties.password = {
499
+ title: intl.formatMessage(messages.addUserFormPasswordTitle),
500
+ type: 'password',
501
+ description: intl.formatMessage(messages.addUserFormPasswordDescription),
502
+ widget: 'password',
503
+ };
504
+
505
+ adduserschema.properties.sendPasswordReset = {
506
+ title: intl.formatMessage(messages.addUserFormSendPasswordResetTitle),
507
+ type: 'boolean',
508
+ };
509
+
510
+ adduserschema.properties.roles = {
511
+ title: intl.formatMessage(messages.addUserFormRolesTitle),
512
+ type: 'array',
513
+ choices: canAssignAdd(isUserManager).map((role) => [role.id, role.title]),
514
+ noValueOption: false,
515
+ };
516
+
517
+ adduserschema.properties.groups = {
518
+ title: intl.formatMessage(messages.addUserGroupNameTitle),
519
+ type: 'array',
520
+ choices: groups
521
+ .filter((group) => canAssignGroup(isUserManager, group))
522
+ .map((group) => [group.id, group.id]),
523
+ noValueOption: false,
524
+ };
525
+ // Add custom fields to the first fieldset if they don't already exist
526
+ if (
527
+ adduserschema.fieldsets &&
528
+ adduserschema.fieldsets.length > 0 &&
529
+ !adduserschema.fieldsets[0].fields.includes('username')
530
+ ) {
531
+ adduserschema.fieldsets[0].fields =
532
+ adduserschema.fieldsets[0].fields.concat([
533
533
  'username',
534
534
  'password',
535
535
  'sendPasswordReset',
536
536
  'roles',
537
537
  'groups',
538
538
  ]);
539
- }
540
539
  }
540
+ }
541
541
 
542
- return (
543
- <Container className="users-control-panel">
544
- <Helmet title={this.props.intl.formatMessage(messages.users)} />
545
- <div className="container">
546
- <Confirm
547
- open={this.state.showDelete}
548
- header={this.props.intl.formatMessage(
549
- messages.deleteUserConfirmTitle,
550
- )}
551
- content={
552
- <div className="content">
553
- <Dimmer active={this.props?.deleteRequest?.loading}>
554
- <Loader>
555
- <FormattedMessage id="Loading" defaultMessage="Loading." />
556
- </Loader>
557
- </Dimmer>
558
-
559
- <ul className="content">
560
- <FormattedMessage
561
- id="Do you really want to delete the user {username}?"
562
- defaultMessage="Do you really want to delete the user {username}?"
563
- values={{
564
- username: <b>{usernameToDelete}</b>,
565
- }}
566
- />
567
- </ul>
568
- </div>
569
- }
570
- onCancel={this.onDeleteCancel}
571
- onConfirm={this.onDeleteOk}
572
- size={null}
573
- />
574
- {this.props?.userschema?.loaded && this.state.showAddUser ? (
575
- <ModalForm
576
- open={this.state.showAddUser}
577
- className="modal"
578
- onSubmit={this.onAddUserSubmit}
579
- submitError={this.state.addUserError}
580
- onCancel={() =>
581
- this.setState({ showAddUser: false, addUserError: undefined })
582
- }
583
- title={this.props.intl.formatMessage(messages.addUserFormTitle)}
584
- loading={this.props.createRequest.loading}
585
- schema={adduserschema}
586
- />
587
- ) : null}
588
- </div>
589
- <Segment.Group raised>
590
- <Segment className="primary">
591
- <FormattedMessage id="Users" defaultMessage="Users" />
592
- </Segment>
593
- <Segment secondary>
594
- <FormattedMessage
595
- id="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
596
- defaultMessage="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
597
- values={{
598
- plone_svg: (
599
- <Icon
600
- name={ploneSVG}
601
- size="20px"
602
- color="#007EB1"
603
- title={'plone-svg'}
604
- />
605
- ),
606
- }}
607
- />
608
- </Segment>
609
- <Segment>
610
- <Form onSubmit={this.onSearch}>
611
- <Form.Field>
612
- <Input
613
- name="SearchableText"
614
- action={{
615
- icon: 'search',
616
- loading: this.state.isLoading,
617
- disabled: this.state.isLoading,
542
+ return (
543
+ <Container className="users-control-panel">
544
+ <Helmet title={intl.formatMessage(messages.users)} />
545
+ <div className="container">
546
+ <Confirm
547
+ open={showDelete}
548
+ header={intl.formatMessage(messages.deleteUserConfirmTitle)}
549
+ content={
550
+ <div className="content">
551
+ <Dimmer active={deleteRequest?.loading}>
552
+ <Loader>
553
+ <FormattedMessage id="Loading" defaultMessage="Loading." />
554
+ </Loader>
555
+ </Dimmer>
556
+
557
+ <ul className="content">
558
+ <FormattedMessage
559
+ id="Do you really want to delete the user {username}?"
560
+ defaultMessage="Do you really want to delete the user {username}?"
561
+ values={{
562
+ username: <b>{usernameToDelete}</b>,
618
563
  }}
619
- placeholder={this.props.intl.formatMessage(
620
- messages.searchUsers,
621
- )}
622
- onChange={this.onChangeSearch}
623
- id="user-search-input"
624
564
  />
625
- </Form.Field>
626
- </Form>
627
- </Segment>
628
- <Form>
629
- {((this.props.many_users && this.state.entries.length > 0) ||
630
- !this.props.many_users) && (
631
- <Table padded striped attached unstackable>
632
- <Table.Header>
633
- <Table.Row>
634
- <Table.HeaderCell>
635
- <FormattedMessage
636
- id="User name"
637
- defaultMessage="User name"
638
- />
639
- </Table.HeaderCell>
640
- {this.props.roles.map((role) => (
641
- <Table.HeaderCell key={role.id}>
642
- {role.title}
643
- </Table.HeaderCell>
644
- ))}
645
- <Table.HeaderCell>
646
- <FormattedMessage id="Actions" defaultMessage="Actions" />
647
- </Table.HeaderCell>
648
- </Table.Row>
649
- </Table.Header>
650
- <Table.Body data-user="users">
651
- {this.state.entries
652
- .slice(
653
- this.state.currentPage * 10,
654
- this.state.pageSize * (this.state.currentPage + 1),
655
- )
656
- .map((user) => (
657
- <RenderUsers
658
- key={user.id}
659
- onDelete={this.delete}
660
- roles={this.props.roles}
661
- user={user}
662
- updateUser={this.updateUserRole}
663
- inheritedRole={this.props.inheritedRole}
664
- userschema={this.props.userschema}
665
- listUsers={this.props.listUsers}
666
- isUserManager={isUserManager}
667
- />
668
- ))}
669
- </Table.Body>
670
- </Table>
671
- )}
672
- {this.state.entries.length === 0 && this.state.search && (
673
- <Segment>
674
- {this.props.intl.formatMessage(messages.userSearchNoResults)}
675
- </Segment>
676
- )}
677
- <div className="contents-pagination">
678
- <Pagination
679
- current={this.state.currentPage}
680
- total={Math.ceil(
681
- this.state.entries?.length / this.state.pageSize,
682
- )}
683
- onChangePage={this.onChangePage}
684
- />
565
+ </ul>
685
566
  </div>
567
+ }
568
+ onCancel={onDeleteCancel}
569
+ onConfirm={onDeleteOk}
570
+ size={null}
571
+ />
572
+ {userschema?.loaded && showAddUser ? (
573
+ <ModalForm
574
+ open={showAddUser}
575
+ className="modal"
576
+ onSubmit={onAddUserSubmit}
577
+ submitError={addUserError}
578
+ onCancel={() => {
579
+ setShowAddUser(false);
580
+ setAddUserError(undefined);
581
+ }}
582
+ title={intl.formatMessage(messages.addUserFormTitle)}
583
+ loading={createRequest?.loading}
584
+ schema={adduserschema}
585
+ />
586
+ ) : null}
587
+ </div>
588
+ <Segment.Group raised>
589
+ <Segment className="primary">
590
+ <FormattedMessage id="Users" defaultMessage="Users" />
591
+ </Segment>
592
+ <Segment secondary>
593
+ <FormattedMessage
594
+ id="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
595
+ defaultMessage="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
596
+ values={{
597
+ plone_svg: (
598
+ <Icon
599
+ name={ploneSVG}
600
+ size="20px"
601
+ color="#007EB1"
602
+ title={'plone-svg'}
603
+ />
604
+ ),
605
+ }}
606
+ />
607
+ </Segment>
608
+ <Segment>
609
+ <Form onSubmit={onSearch}>
610
+ <Form.Field>
611
+ <Input
612
+ name="SearchableText"
613
+ action={{
614
+ icon: 'search',
615
+ loading: isLoading,
616
+ disabled: isLoading,
617
+ }}
618
+ placeholder={intl.formatMessage(messages.searchUsers)}
619
+ onChange={onChangeSearch}
620
+ id="user-search-input"
621
+ />
622
+ </Form.Field>
686
623
  </Form>
687
- </Segment.Group>
688
- {this.state.isClient &&
689
- createPortal(
690
- <Toolbar
691
- pathname={this.props.pathname}
692
- hideDefaultViewButtons
693
- inner={
694
- <>
695
- <Button
696
- id="toolbar-save"
697
- className="save"
698
- aria-label={this.props.intl.formatMessage(messages.save)}
699
- onClick={this.updateUserRoleSubmit}
700
- loading={this.props.createRequest.loading}
701
- >
702
- <Icon
703
- name={saveSVG}
704
- className="circled"
705
- size="30px"
706
- title={this.props.intl.formatMessage(messages.save)}
707
- />
708
- </Button>
709
- <Link to="/controlpanel" className="cancel">
710
- <Icon
711
- name={clearSVG}
712
- className="circled"
713
- aria-label={this.props.intl.formatMessage(
714
- messages.cancel,
715
- )}
716
- size="30px"
717
- title={this.props.intl.formatMessage(messages.cancel)}
624
+ </Segment>
625
+ <Form>
626
+ {((many_users && entries.length > 0) || !many_users) && (
627
+ <Table padded striped attached unstackable>
628
+ <Table.Header>
629
+ <Table.Row>
630
+ <Table.HeaderCell>
631
+ <FormattedMessage
632
+ id="User name"
633
+ defaultMessage="User name"
718
634
  />
719
- </Link>
720
- <Button
721
- id="toolbar-add"
722
- aria-label={this.props.intl.formatMessage(
723
- messages.addUserButtonTitle,
724
- )}
725
- onClick={() => {
726
- this.setState({ showAddUser: true });
727
- }}
728
- loading={this.props.createRequest.loading}
729
- >
730
- <Icon
731
- name={addUserSvg}
732
- size="45px"
733
- color="#826A6A"
734
- title={this.props.intl.formatMessage(
735
- messages.addUserButtonTitle,
736
- )}
635
+ </Table.HeaderCell>
636
+ {roles.map((role) => (
637
+ <Table.HeaderCell key={role.id}>
638
+ {role.title}
639
+ </Table.HeaderCell>
640
+ ))}
641
+ <Table.HeaderCell>
642
+ <FormattedMessage id="Actions" defaultMessage="Actions" />
643
+ </Table.HeaderCell>
644
+ </Table.Row>
645
+ </Table.Header>
646
+ <Table.Body data-user="users">
647
+ {entries
648
+ .slice(currentPage * 10, pageSize * (currentPage + 1))
649
+ .map((userItem) => (
650
+ <RenderUsers
651
+ key={userItem.id}
652
+ onDelete={handleDeleteUser}
653
+ roles={roles}
654
+ user={userItem}
655
+ updateUser={updateUserRole}
656
+ inheritedRole={inheritedRole}
657
+ userschema={userschema}
658
+ listUsers={listUsersAction}
659
+ isUserManager={isUserManager}
737
660
  />
738
- </Button>
739
- </>
740
- }
741
- />,
742
- document.getElementById('toolbar'),
661
+ ))}
662
+ </Table.Body>
663
+ </Table>
743
664
  )}
744
- </Container>
745
- );
746
- }
747
- }
748
-
749
- export default compose(
750
- injectIntl,
751
- connect(
752
- (state, props) => ({
753
- roles: state.roles.roles,
754
- users: state.users.users,
755
- user: state.users.user,
756
- userId: state.userSession.token
757
- ? jwtDecode(state.userSession.token).sub
758
- : '',
759
- groups: state.groups.groups,
760
- many_users: state.controlpanels?.controlpanel?.data?.many_users,
761
- many_groups: state.controlpanels?.controlpanel?.data?.many_groups,
762
- description: state.description,
763
- pathname: props.location.pathname,
764
- deleteRequest: state.users.delete,
765
- createRequest: state.users.create,
766
- loadRolesRequest: state.roles,
767
- inheritedRole: state.authRole.authenticatedRole,
768
- userschema: state.userschema,
769
- controlPanelData: state.controlpanels?.controlpanel,
770
- }),
771
- (dispatch) =>
772
- bindActionCreators(
773
- {
774
- listRoles,
775
- listUsers,
776
- listGroups,
777
- getControlpanel,
778
- deleteUser,
779
- createUser,
780
- updateUser,
781
- updateGroup,
782
- getUserSchema,
783
- getUser,
784
- },
785
- dispatch,
786
- ),
787
- ),
788
- )(UsersControlpanel);
665
+ {entries.length === 0 && search && (
666
+ <Segment>
667
+ {intl.formatMessage(messages.userSearchNoResults)}
668
+ </Segment>
669
+ )}
670
+ <div className="contents-pagination">
671
+ <Pagination
672
+ current={currentPage}
673
+ total={Math.ceil(entries?.length / pageSize)}
674
+ onChangePage={onChangePage}
675
+ />
676
+ </div>
677
+ </Form>
678
+ </Segment.Group>
679
+ {isClient &&
680
+ createPortal(
681
+ <Toolbar
682
+ pathname={pathname}
683
+ hideDefaultViewButtons
684
+ inner={
685
+ <>
686
+ <Button
687
+ id="toolbar-save"
688
+ className="save"
689
+ aria-label={intl.formatMessage(messages.save)}
690
+ onClick={updateUserRoleSubmit}
691
+ loading={createRequest?.loading}
692
+ >
693
+ <Icon
694
+ name={saveSVG}
695
+ className="circled"
696
+ size="30px"
697
+ title={intl.formatMessage(messages.save)}
698
+ />
699
+ </Button>
700
+ <Link to="/controlpanel" className="cancel">
701
+ <Icon
702
+ name={clearSVG}
703
+ className="circled"
704
+ aria-label={intl.formatMessage(messages.cancel)}
705
+ size="30px"
706
+ title={intl.formatMessage(messages.cancel)}
707
+ />
708
+ </Link>
709
+ <Button
710
+ id="toolbar-add"
711
+ aria-label={intl.formatMessage(messages.addUserButtonTitle)}
712
+ onClick={() => {
713
+ setShowAddUser(true);
714
+ }}
715
+ loading={createRequest?.loading}
716
+ >
717
+ <Icon
718
+ name={addUserSvg}
719
+ size="45px"
720
+ color="#826A6A"
721
+ title={intl.formatMessage(messages.addUserButtonTitle)}
722
+ />
723
+ </Button>
724
+ </>
725
+ }
726
+ />,
727
+ document.getElementById('toolbar'),
728
+ )}
729
+ </Container>
730
+ );
731
+ };
732
+
733
+ export default UsersControlpanel;