UNPKG

@plone/volto

Version:
565 lines (547 loc) 18.4 kB
/** * Sharing container. * @module components/manage/Sharing/Sharing */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Plug, Pluggable } from '@plone/volto/components/manage/Pluggable'; import Helmet from '@plone/volto/helpers/Helmet/Helmet'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link, withRouter } from 'react-router-dom'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import map from 'lodash/map'; import { createPortal } from 'react-dom'; import { Button, Checkbox, Container as SemanticContainer, Form, Icon as IconOld, Input, Segment, Table, } from 'semantic-ui-react'; import jwtDecode from 'jwt-decode'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { updateSharing, getSharing, } from '@plone/volto/actions/sharing/sharing'; import { getBaseUrl } from '@plone/volto/helpers/Url/Url'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import Toast from '@plone/volto/components/manage/Toast/Toast'; import { toast } from 'react-toastify'; import config from '@plone/volto/registry'; import aheadSVG from '@plone/volto/icons/ahead.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import backSVG from '@plone/volto/icons/back.svg'; const messages = defineMessages({ searchForUserOrGroup: { id: 'Search for user or group', defaultMessage: 'Search for user or group', }, search: { id: 'Search', defaultMessage: 'Search', }, inherit: { id: 'Inherit permissions from higher levels', defaultMessage: 'Inherit permissions from higher levels', }, save: { id: 'Save', defaultMessage: 'Save', }, cancel: { id: 'Cancel', defaultMessage: 'Cancel', }, back: { id: 'Back', defaultMessage: 'Back', }, sharing: { id: 'Sharing', defaultMessage: 'Sharing', }, user: { id: 'User', defaultMessage: 'User', }, group: { id: 'Group', defaultMessage: 'Group', }, globalRole: { id: 'Global role', defaultMessage: 'Global role', }, inheritedValue: { id: 'Inherited value', defaultMessage: 'Inherited value', }, permissionsUpdated: { id: 'Permissions updated', defaultMessage: 'Permissions updated', }, permissionsUpdatedSuccessfully: { id: 'Permissions have been updated successfully', defaultMessage: 'Permissions have been updated successfully', }, assignNewRoles: { id: 'Assign the {role} role to {entry}', defaultMessage: 'Assign the {role} role to {entry}', }, }); /** * SharingComponent class. * @class SharingComponent * @extends Component */ class SharingComponent extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { updateSharing: PropTypes.func.isRequired, getSharing: PropTypes.func.isRequired, updateRequest: PropTypes.shape({ loading: PropTypes.bool, loaded: PropTypes.bool, }).isRequired, pathname: PropTypes.string.isRequired, entries: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string, login: PropTypes.string, roles: PropTypes.object, title: PropTypes.string, type: PropTypes.string, }), ).isRequired, available_roles: PropTypes.arrayOf(PropTypes.object).isRequired, inherit: PropTypes.bool, title: PropTypes.string.isRequired, login: PropTypes.string, }; /** * Default properties * @property {Object} defaultProps Default properties. * @static */ static defaultProps = { inherit: null, login: '', }; /** * Constructor * @method constructor * @param {Object} props Component properties * @constructs Sharing */ constructor(props) { super(props); this.onCancel = this.onCancel.bind(this); this.onChange = this.onChange.bind(this); this.onChangeSearch = this.onChangeSearch.bind(this); this.onSearch = this.onSearch.bind(this); this.onSubmit = this.onSubmit.bind(this); this.onToggleInherit = this.onToggleInherit.bind(this); this.state = { search: '', isLoading: false, inherit: props.inherit, entries: props.entries, isClient: false, }; } /** * Component did mount * @method componentDidMount * @returns {undefined} */ componentDidMount() { this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search); this.setState({ isClient: true }); } /** * Component will receive props * @method componentWillReceiveProps * @param {Object} nextProps Next properties * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) { this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search); toast.success( <Toast success title={this.props.intl.formatMessage(messages.permissionsUpdated)} content={this.props.intl.formatMessage( messages.permissionsUpdatedSuccessfully, )} />, ); } this.setState({ inherit: this.props.inherit === null ? nextProps.inherit : this.state.inherit, entries: map(nextProps.entries, (entry) => { const values = find(this.state.entries, { id: entry.id }); return { ...entry, roles: values ? values.roles : entry.roles, }; }), }); } /** * Submit handler * @method onSubmit * @param {object} event Event object. * @returns {undefined} */ onSubmit(event) { const data = { entries: [] }; event.preventDefault(); if (this.props.inherit !== this.state.inherit) { data.inherit = this.state.inherit; } for (let i = 0; i < this.props.entries.length; i += 1) { if (!isEqual(this.props.entries[i].roles, this.state.entries[i].roles)) { data.entries.push({ id: this.state.entries[i].id, type: this.state.entries[i].type, roles: this.state.entries[i].roles, }); } } this.props.updateSharing(getBaseUrl(this.props.pathname), data); } /** * Search handler * @method onSearch * @param {object} event Event object. * @returns {undefined} */ onSearch(event) { event.preventDefault(); this.setState({ isLoading: true }); this.props .getSharing(getBaseUrl(this.props.pathname), this.state.search) .then(() => { this.setState({ isLoading: false }); }) .catch((error) => { this.setState({ isLoading: false }); // eslint-disable-next-line no-console console.error('Error searching users or groups', error); }); } /** * On change search handler * @method onChangeSearch * @param {object} event Event object. * @returns {undefined} */ onChangeSearch(event) { this.setState({ search: event.target.value, }); } /** * On toggle inherit handler * @method onToggleInherit * @returns {undefined} */ onToggleInherit() { this.setState((state) => ({ inherit: !state.inherit, })); } /** * On change handler * @method onChange * @param {object} event Event object * @param {string} value Entry value * @returns {undefined} */ onChange(event, { value }) { const [principal, role] = value.split(':'); this.setState({ entries: map(this.state.entries, (entry) => ({ ...entry, roles: entry.id === principal ? { ...entry.roles, [role]: !entry.roles[role], } : entry.roles, })), }); } /** * Cancel handler * @method onCancel * @returns {undefined} */ onCancel() { this.props.history.push(getBaseUrl(this.props.pathname)); } /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; return ( <Container id="page-sharing"> <Helmet title={this.props.intl.formatMessage(messages.sharing)} /> <Segment.Group raised> <Pluggable name="sharing-component" params={{ isLoading: this.state.isLoading }} /> <Plug pluggable="sharing-component" id="sharing-component-title"> <Segment className="primary"> <FormattedMessage id="Sharing for {title}" defaultMessage="Sharing for {title}" values={{ title: <q>{this.props.title}</q> }} /> </Segment> </Plug> <Plug pluggable="sharing-component" id="sharing-component-description" > <Segment secondary> <FormattedMessage id="You can control who can view and edit your item using the list below." defaultMessage="You can control who can view and edit your item using the list below." /> </Segment> </Plug> <Plug pluggable="sharing-component" id="sharing-component-search"> {({ isLoading }) => { return ( <Segment> <Form onSubmit={this.onSearch}> <Form.Field> <Input name="SearchableText" action={{ icon: 'search', loading: isLoading, disabled: isLoading, 'aria-label': this.props.intl.formatMessage( messages.search, ), }} placeholder={this.props.intl.formatMessage( messages.searchForUserOrGroup, )} onChange={this.onChangeSearch} id="sharing-component-search" /> </Form.Field> </Form> </Segment> ); }} </Plug> <Plug pluggable="sharing-component" id="sharing-component-form" dependencies={[this.state.entries, this.props.available_roles]} > <Form onSubmit={this.onSubmit}> <Table celled padded striped attached> <Table.Header> <Table.Row> <Table.HeaderCell> <FormattedMessage id="Name" defaultMessage="Name" /> </Table.HeaderCell> {this.props.available_roles?.map((role) => ( <Table.HeaderCell key={role.id}> {role.title} </Table.HeaderCell> ))} </Table.Row> </Table.Header> <Table.Body> {this.state.entries?.map((entry) => ( <Table.Row key={entry.id}> <Table.Cell> <IconOld name={entry.type === 'user' ? 'user' : 'users'} title={ entry.type === 'user' ? this.props.intl.formatMessage(messages.user) : this.props.intl.formatMessage(messages.group) } />{' '} {entry.title} {entry.login && ` (${entry.login})`} </Table.Cell> {this.props.available_roles?.map((role) => ( <Table.Cell key={role.id}> {entry.roles[role.id] === 'global' && ( <IconOld name="check circle outline" title={this.props.intl.formatMessage( messages.globalRole, )} color="blue" /> )} {entry.roles[role.id] === 'acquired' && ( <IconOld name="check circle outline" color="green" title={this.props.intl.formatMessage( messages.inheritedValue, )} /> )} {typeof entry.roles[role.id] === 'boolean' && ( <Checkbox name={this.props.intl.formatMessage( messages.assignNewRoles, { entry: entry.title, role: role.id, }, )} aria-label={this.props.intl.formatMessage( messages.assignNewRoles, { entry: entry.title, role: role.id, }, )} onChange={this.onChange} value={`${entry.id}:${role.id}`} checked={entry.roles[role.id]} disabled={entry.login === this.props.login} /> )} </Table.Cell> ))} </Table.Row> ))} </Table.Body> </Table> <Segment attached> <Form.Field> <Checkbox id="inherit-permissions-checkbox" name="inherit-permissions-checkbox" defaultChecked={this.state.inherit} onChange={this.onToggleInherit} label={ <label htmlFor="inherit-permissions-checkbox"> {this.props.intl.formatMessage(messages.inherit)} </label> } /> </Form.Field> <p className="help"> <FormattedMessage id="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator." defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, inherited values are explicitly labeled as 'Inherited value' and receive a green check mark {inherited}. Similarly, roles managed by the site administrator are labeled as 'Global role' and receive a blue check mark {global}." values={{ inherited: ( <IconOld aria-hidden="true" name="check circle outline" color="green" /> ), global: ( <IconOld aria-hidden="true" name="check circle outline" color="blue" /> ), }} /> </p> </Segment> <Segment className="right aligned actions" attached clearing> <Button basic secondary aria-label={this.props.intl.formatMessage(messages.cancel)} title={this.props.intl.formatMessage(messages.cancel)} onClick={this.onCancel} > <Icon className="circled" name={clearSVG} size="30px" /> </Button> <Button basic primary type="submit" aria-label={this.props.intl.formatMessage(messages.save)} title={this.props.intl.formatMessage(messages.save)} loading={this.props.updateRequest.loading} onClick={this.onSubmit} > <Icon className="circled" name={aheadSVG} size="30px" /> </Button> </Segment> </Form> </Plug> </Segment.Group> {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={ <Link to={`${getBaseUrl(this.props.pathname)}`} className="item" > <Icon name={backSVG} className="contents circled" size="30px" title={this.props.intl.formatMessage(messages.back)} /> </Link> } />, document.getElementById('toolbar'), )} </Container> ); } } export default compose( withRouter, injectIntl, connect( (state, props) => ({ entries: state.sharing.data.entries, inherit: state.sharing.data.inherit, available_roles: state.sharing.data.available_roles, updateRequest: state.sharing.update, pathname: props.location.pathname, title: state.content.data.title, login: state.userSession.token ? jwtDecode(state.userSession.token).sub : '', }), { updateSharing, getSharing }, ), )(SharingComponent);