diff --git a/RELEASE.md b/RELEASE.md index 1c179dab9..175a50e05 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -67,7 +67,7 @@ ### Fix -- ... +- L'assegnazione dei ruoli nella vista gruppi funziona correttamente per tutti i gruppi di utenti. ## Versione 11.23.1 (19/09/2024) diff --git a/src/customizations/volto/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx b/src/customizations/volto/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx new file mode 100644 index 000000000..2afb4b07e --- /dev/null +++ b/src/customizations/volto/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx @@ -0,0 +1,709 @@ +// CUSTOMIZATION: +// - 124: Set AuthenticatedUsers roles as authenticatedRole for all groups +// - 263: Changed updateGroupRole function to update all checkboxes when changing the AuthenticatedUsers checkboxes + +/** + * Users controlpanel container. + * @module components/manage/Controlpanels/UsersControlpanel + */ +import { + createGroup, + deleteGroup, + listGroups, + getControlpanel, + listRoles, + updateGroup, + authenticatedRole, +} from '@plone/volto/actions'; +import { + Icon, + ModalForm, + Toast, + Toolbar, + RenderGroups, + Pagination, + Error, +} from '@plone/volto/components'; +import { Link } from 'react-router-dom'; +import { Helmet, messages } from '@plone/volto/helpers'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import addUserSvg from '@plone/volto/icons/add-user.svg'; +import saveSVG from '@plone/volto/icons/save.svg'; +import ploneSVG from '@plone/volto/icons/plone.svg'; +import { find, map } from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { Portal } from 'react-portal'; +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; +import { bindActionCreators, compose } from 'redux'; +import { + Confirm, + Container, + Button, + Form, + Input, + Segment, + Table, +} from 'semantic-ui-react'; + +/** + * GroupsControlpanel class. + * @class GroupsControlpanel + * @extends Component + */ +class GroupsControlpanel extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + listRoles: PropTypes.func.isRequired, + listGroups: PropTypes.func.isRequired, + pathname: PropTypes.string.isRequired, + roles: PropTypes.arrayOf( + PropTypes.shape({ + '@id': PropTypes.string, + '@type': PropTypes.string, + id: PropTypes.string, + }), + ).isRequired, + groups: PropTypes.arrayOf( + PropTypes.shape({ + Title: PropTypes.string, + Description: PropTypes.string, + roles: PropTypes.arrayOf(PropTypes.string), + groupname: PropTypes.string, + }), + ).isRequired, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs Sharing + */ + constructor(props) { + super(props); + this.onChangeSearch = this.onChangeSearch.bind(this); + this.onSearchGroups = this.onSearchGroups.bind(this); + this.deleteGroup = this.deleteGroup.bind(this); + this.onDeleteOk = this.onDeleteOk.bind(this); + this.onDeleteCancel = this.onDeleteCancel.bind(this); + this.onAddGroupSubmit = this.onAddGroupSubmit.bind(this); + this.onAddGroupError = this.onAddGroupError.bind(this); + this.onAddGroupSuccess = this.onAddGroupSuccess.bind(this); + this.updateGroupRole = this.updateGroupRole.bind(this); + this.state = { + search: '', + isLoading: false, + addGroupError: '', + showDelete: false, + groupToDelete: undefined, + showAddGroup: false, + groupEntries: [], + isClient: false, + authenticatedRole: props.inheritedRole || [], + currentPage: 0, + pageSize: 10, + }; + } + + fetchData = async () => { + await this.props.getControlpanel('usergroup'); + await this.props.listRoles(); + if (!this.props.many_groups) { + await this.props.listGroups(); + const inheritedRoles = this.props.groups?.find( + (el) => el.id === 'AuthenticatedUsers', + )?.roles; + this.setState({ + groupEntries: this.props.groups, + authenticatedRole: inheritedRoles, + }); + } + }; + /** + * Component did mount + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + this.setState({ + isClient: true, + }); + this.fetchData(); + } + + UNSAFE_componentWillReceiveProps(nextProps) { + if ( + (this.props.deleteGroupRequest.loading && + nextProps.deleteGroupRequest.loaded) || + (this.props.createGroupRequest.loading && + nextProps.createGroupRequest.loaded) + ) { + this.props.listGroups(this.state.search); + } + if ( + this.props.createGroupRequest.loading && + nextProps.createGroupRequest.loaded + ) { + this.onAddGroupSuccess(); + } + if ( + this.props.createGroupRequest.loading && + nextProps.createGroupRequest.error + ) { + this.onAddGroupError(nextProps.createGroupRequest.error); + } + if ( + this.props.loadRolesRequest.loading && + nextProps.loadRolesRequest.error + ) { + this.setState({ + error: nextProps.loadRolesRequest.error, + }); + } + } + + getGroupFromProps(value) { + return find(this.props.groups, ['@id', value]); + } + + /** + * + * + * @param {*} event Event object + * @memberof GroupsControlpanel + * @returns {undefined} + */ + onSearchGroups(event) { + this.setState({ isLoading: true }); + event.preventDefault(); + this.props + .listGroups(this.state.search) + .then(() => { + this.setState({ isLoading: false }); + }) + .catch((error) => { + this.setState({ isLoading: false }); + // eslint-disable-next-line no-console + console.error('Error searching group', error); + }); + } + + /** + * On change search handler + * @method onChangeSearch + * @param {object} event Event object. + * @returns {undefined} + */ + onChangeSearch(event) { + this.setState({ + search: event.target.value, + }); + } + + /** + * + * + * @param {*} event Event object. + * @param {*} { value } id (groupname) + * @memberof GroupsControlpanel + * @returns {undefined} + */ + deleteGroup(event, { value }) { + if (value) { + this.setState({ + showDelete: true, + groupToDelete: this.getGroupFromProps(value), + }); + } + } + + /** + * On delete ok + * @method onDeleteOk + * @returns {undefined} + */ + onDeleteOk() { + if (this.state.groupToDelete) { + this.props.deleteGroup(this.state.groupToDelete.id); + this.setState({ + showDelete: false, + groupToDelete: undefined, + }); + } + } + + /** + * On delete cancel + * @method onDeleteCancel + * @returns {undefined} + */ + onDeleteCancel() { + this.setState({ + showDelete: false, + itemsToDelete: [], + }); + } + + /** + * + * @param {*} name + * @param {*} value + * @memberof GroupsControlpanel + */ + + updateGroupRole(name, value) { + this.setState((prevState) => { + let updatedAuthenticatedRole; + // check if checkbox is AuthenticatedUsers + if (name === 'AuthenticatedUsers') { + // check if that role is already set + updatedAuthenticatedRole = prevState.authenticatedRole.includes(value) + ? // Remove role if unchecked + prevState.authenticatedRole.filter((role) => role !== value) + : // Add role if checked + [...prevState.authenticatedRole, value]; + } else { + // if not AuthenticatedUser, do not change the authenticated roles + updatedAuthenticatedRole = prevState.authenticatedRole; + } + + // Handle the groupEntries role update + const updatedGroupEntries = map(prevState.groupEntries, (entry) => { + if (entry.id === name) { + if (entry.roles.includes(value)) { + // If the role is unchecked, remove it from roles + return { + ...entry, + roles: entry.roles.filter((role) => role !== value), + }; + } else { + // If the role is checked, add it to roles + return { + ...entry, + roles: [...entry.roles, value], + }; + } + } else if ( + name === 'AuthenticatedUsers' && + !prevState.authenticatedRole.includes(value) + ) { + // If 'AuthenticatedUsers' is unchecked, reset the other group roles + return { + ...entry, + roles: entry.roles.filter((role) => role !== value), // Remove the role from all other groups + }; + } + + return entry; // For other entries, return unchanged + }); + + return { + groupEntries: updatedGroupEntries, + authenticatedRole: updatedAuthenticatedRole, + }; + }); + } + /** + * @param {*} event + * @memberof GroupsControlpanel + */ + updateGroupRoleSubmit = (e) => { + e.stopPropagation(); + this.state.groupEntries.forEach((item) => { + this.props.updateGroup(item.id, item); + }); + this.props.authenticatedRole(this.state.authenticatedRole); + toast.success( + , + ); + }; + /** + * + * + * @param {object} data Form data from the ModalForm. + * @param {func} callback to set new form data in the ModalForm + * @memberof GroupsControlpanel + * @returns {undefined} + */ + onAddGroupSubmit(data, callback) { + this.props.createGroup(data); + this.setState({ + addGroupSetFormDataCallback: callback, + }); + } + + /** + * Handle Errors after createGroup() + * + * @param {*} error object. Requires the property .message + * @memberof GroupsControlpanel + * @returns {undefined} + */ + onAddGroupError(error) { + this.setState({ + addGroupError: error.response.body.message, + }); + } + + componentDidUpdate(prevProps, prevState) { + if (this.props.groups !== prevProps.groups) { + this.setState({ + groupEntries: this.props.groups, + }); + } + } + + /** + * Handle Success after createGroup() + * + * @memberof GroupsControlpanel + * @returns {undefined} + */ + onAddGroupSuccess() { + this.state.addGroupSetFormDataCallback({}); + this.setState({ + showAddGroup: false, + addGroupError: undefined, + addGroupSetFormDataCallback: undefined, + }); + toast.success( + , + ); + } + + /** + * On change page + * @method onChangePage + * @param {object} event Event object. + * @param {string} value Page value. + * @returns {undefined} + */ + onChangePage = (event, { value }) => { + this.setState({ + currentPage: value, + }); + }; + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + if (this.state.error) { + return ; + } + /*let fullnameToDelete = this.state.groupToDelete + ? this.state.groupToDelete.fullname + : '';*/ + let groupNameToDelete = this.state.groupToDelete + ? this.state.groupToDelete.id + : ''; + + return ( + + +
+ +
    + {groupNameToDelete}, + }} + /> +
+
+ } + onCancel={this.onDeleteCancel} + onConfirm={this.onDeleteOk} + size={null} + /> + {this.state.showAddGroup ? ( + this.setState({ showAddGroup: false })} + title={this.props.intl.formatMessage(messages.addGroupsFormTitle)} + loading={this.props.createGroupRequest.loading} + schema={{ + fieldsets: [ + { + id: 'default', + title: 'FIXME: Group Data', + fields: [ + 'title', + 'description', + 'groupname', + 'email', + 'roles', + ], + }, + ], + properties: { + title: { + title: this.props.intl.formatMessage( + messages.addGroupsFormTitleTitle, + ), + type: 'string', + description: '', + }, + description: { + title: this.props.intl.formatMessage( + messages.addGroupsFormDescriptionTitle, + ), + type: 'string', + description: '', + }, + groupname: { + title: this.props.intl.formatMessage( + messages.addGroupsFormGroupNameTitle, + ), + type: 'string', + description: + 'A unique identifier for the group. Can not be changed after creation.', + }, + email: { + title: this.props.intl.formatMessage( + messages.addGroupsFormEmailTitle, + ), + type: 'string', + description: '', + widget: 'email', + }, + roles: { + title: this.props.intl.formatMessage( + messages.addGroupsFormRolesTitle, + ), + type: 'array', + choices: this.props.roles.map((role) => [ + role.id, + role.title, + ]), + noValueOption: false, + description: '', + }, + }, + required: ['groupname'], + }} + /> + ) : null} + + + + + + + + ), + }} + /> + + +
+ + + +
+
+
+
+ {((this.props.many_groups && + this.state.groupEntries.length > 0) || + !this.props.many_groups) && ( + + + + + + + {this.props.roles.map((role) => ( + + {role.title} + + ))} + + + + + + + {this.state.groupEntries + .slice( + this.state.currentPage * 10, + this.state.pageSize * (this.state.currentPage + 1), + ) + .map((group) => ( + + ))} + +
+ )} + {this.state.groupEntries.length === 0 && this.state.search && ( + + {this.props.intl.formatMessage(messages.groupSearchNoResults)} + + )} +
+
+ +
+
+
+ {this.state.isClient && ( + + + + + + + + + } + /> + + )} +
+ ); + } +} + +export default compose( + injectIntl, + connect( + (state, props) => ({ + roles: state.roles.roles, + groups: state.groups.groups, + description: state.description, + many_users: state.controlpanels?.controlpanel?.data?.many_users, + many_groups: state.controlpanels?.controlpanel?.data?.many_groups, + pathname: props.location.pathname, + deleteGroupRequest: state.groups.delete, + createGroupRequest: state.groups.create, + loadRolesRequest: state.roles, + inheritedRole: state.authRole.authenticatedRole, + }), + (dispatch) => + bindActionCreators( + { + listRoles, + listGroups, + deleteGroup, + getControlpanel, + createGroup, + updateGroup, + authenticatedRole, + }, + dispatch, + ), + ), +)(GroupsControlpanel); diff --git a/src/customizations/volto/components/manage/Controlpanels/Groups/RenderGroups.jsx b/src/customizations/volto/components/manage/Controlpanels/Groups/RenderGroups.jsx new file mode 100644 index 000000000..7418698ed --- /dev/null +++ b/src/customizations/volto/components/manage/Controlpanels/Groups/RenderGroups.jsx @@ -0,0 +1,122 @@ +// CUSTOMIZATION: +// - 97: changed condition check or uncheck Checkboxes +/** + * Users controlpanel groups. + * @module components/manage/Controlpanels/UsersControlpanelGroups + */ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { Dropdown, Table, Checkbox } from 'semantic-ui-react'; +import trashSVG from '@plone/volto/icons/delete.svg'; +import ploneSVG from '@plone/volto/icons/plone.svg'; +import { Icon } from '@plone/volto/components'; + +/** + * UsersControlpanelGroups class. + * @class UsersControlpanelGroups + * @extends Component + */ +class RenderGroups extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + //single group + group: PropTypes.shape({ + title: PropTypes.string, + description: PropTypes.string, + email: PropTypes.string, + groupname: PropTypes.string, + roles: PropTypes.arrayOf(PropTypes.string), + }).isRequired, + + roles: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + }), + ).isRequired, + inheritedRole: PropTypes.array, + onDelete: PropTypes.func.isRequired, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs Sharing + */ + constructor(props) { + super(props); + this.onChange = this.onChange.bind(this); + } + + /** + * @param {*} event + * @param {*} { value } + * @memberof UsersControlpanelUser + */ + onChange(event, { value }) { + const [group, role] = value.split('&role='); + this.props.updateGroups(group, role); + } + + /** + *@param {*} + *@returns {Boolean} + *@memberof RenderGroups + */ + isAuthGroup = (roleId) => { + return this.props.inheritedRole.includes(roleId); + }; + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + return ( + + {this.props.group.groupname} + {this.props.roles.map((role) => ( + + {this.props.inheritedRole && + this.props.inheritedRole.includes(role.id) && + this.props.group.roles.includes('Authenticated') ? ( + + ) : ( + + )} + + ))} + + + + + + + + + + + + ); + } +} + +export default injectIntl(RenderGroups);