@plone/volto
Version:
Volto
565 lines (547 loc) • 18.4 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, 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);