UNPKG

@plone/volto

Version:
559 lines (541 loc) 18.2 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, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator." values={{ inherited: ( <IconOld name="check circle outline" color="green" /> ), global: ( <IconOld name="check circle outline" color="blue" /> ), }} /> </p> </Segment> <Segment className="actions" attached clearing> <Button basic primary floated="right" 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> <Button basic secondary aria-label={this.props.intl.formatMessage(messages.cancel)} title={this.props.intl.formatMessage(messages.cancel)} floated="right" onClick={this.onCancel} > <Icon className="circled" name={clearSVG} 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);