@plone/volto
Version:
Volto
559 lines (541 loc) • 18.2 kB
JSX
/**
* 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);