cspace-ui
Version:
CollectionSpace user interface for browsers
474 lines (381 loc) • 12.6 kB
JSX
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';
import { defineMessages, intlShape, FormattedMessage } from 'react-intl';
import get from 'lodash/get';
import { components as inputComponents, helpers as inputHelpers } from 'cspace-input';
import { getRecordTypeConfigByServicePath } from '../../helpers/configHelpers';
import permissionButtonStyles from '../../../styles/cspace-ui/PermissionButton.css';
import styles from '../../../styles/cspace-ui/PermissionsInput.css';
const {
MiniButton,
} = inputComponents;
const {
getPath,
pathPropType,
} = inputHelpers.pathHelpers;
const permMessages = defineMessages({
'': {
id: 'permissionsInput.perm.none',
description: 'Label of the \'none\' permission level shown when editing permissions.',
defaultMessage: 'None',
},
RL: {
id: 'permissionsInput.perm.read',
description: 'Label of the \'read\' permission level shown when editing permissions.',
defaultMessage: 'Read',
},
CRUL: {
id: 'permissionsInput.perm.write',
description: 'Label of the \'write\' permission level shown when editing permissions.',
defaultMessage: 'Write',
},
CRUDL: {
id: 'permissionsInput.perm.delete',
description: 'Label of the \'delete\' permission level shown when editing permissions.',
defaultMessage: 'Delete',
},
});
const serviceTypeMessages = defineMessages({
object: {
id: 'permissionsInput.serviceType.object',
defaultMessage: 'Objects',
},
procedure: {
id: 'permissionsInput.serviceType.procedure',
defaultMessage: 'Procedures',
},
authority: {
id: 'permissionsInput.serviceType.authority',
defaultMessage: 'Authorities',
},
utility: {
id: 'permissionsInput.serviceType.utility',
defaultMessage: 'Utility Resources',
},
security: {
id: 'permissionsInput.serviceType.security',
defaultMessage: 'Security Resources',
},
});
const serviceTypes = ['object', 'procedure', 'authority', 'utility', 'security'];
const propTypes = {
/* eslint-disable react/no-unused-prop-types */
name: PropTypes.string,
parentPath: pathPropType,
subpath: pathPropType,
/* eslint-enable react/no-unused-prop-types */
readOnly: PropTypes.bool,
resourceNames: PropTypes.instanceOf(Immutable.List),
value: PropTypes.oneOfType([
PropTypes.instanceOf(Immutable.List),
PropTypes.instanceOf(Immutable.Map),
]),
readPerms: PropTypes.func,
onCommit: PropTypes.func,
};
const contextTypes = {
intl: intlShape,
config: PropTypes.shape({
recordTypes: PropTypes.object,
}),
};
export default class PermissionsInput extends Component {
constructor(props, context) {
super(props, context);
this.handleHeaderButtonClick = this.handleHeaderButtonClick.bind(this);
this.handleRadioChange = this.handleRadioChange.bind(this);
}
componentDidMount() {
const {
resourceNames,
readPerms,
} = this.props;
const {
config,
} = this.context;
if (!resourceNames && readPerms) {
readPerms(config);
}
}
handleHeaderButtonClick(event) {
const {
servicetype: serviceType,
actiongroup: actionGroup,
} = event.currentTarget.dataset;
const {
onCommit,
} = this.props;
if (onCommit) {
const stagedUpdates = {};
this.getRecordTypeConfigs()
.filter((recordTypeConfig) => recordTypeConfig.serviceConfig.serviceType === serviceType)
.forEach((recordTypeConfig) => {
this.stageUpdate(recordTypeConfig.name, actionGroup, stagedUpdates);
});
const updatedPerms = this.updatePerms(stagedUpdates);
onCommit(getPath(this.props), updatedPerms);
}
}
handleRadioChange(event) {
const {
value: actionGroup,
checked: selected,
} = event.target;
const {
name: recordType,
} = event.target.dataset;
const {
onCommit,
} = this.props;
if (selected && onCommit) {
const stagedUpdates = this.stageUpdate(recordType, actionGroup);
const updatedPerms = this.updatePerms(stagedUpdates);
onCommit(getPath(this.props), updatedPerms);
}
}
getPermsMap() {
const {
value,
} = this.props;
// The services layer gives us a list of resources and action groups. Build a map of resource
// names to UI permissions.
let permissionsList = value;
if (!permissionsList) {
return undefined;
}
if (!Immutable.List.isList(permissionsList)) {
permissionsList = Immutable.List.of(permissionsList);
}
const perms = {};
permissionsList.forEach((permission) => {
const resourceName = permission.get('resourceName');
const actionGroup = permission.get('actionGroup');
perms[resourceName] = actionGroup;
});
return perms;
}
getRecordTypeConfigs() {
const {
config,
} = this.context;
const {
resourceNames,
} = this.props;
return (
resourceNames
.map((resourceName) => getRecordTypeConfigByServicePath(config, resourceName))
.filter((recordTypeConfig) => (recordTypeConfig && !recordTypeConfig.disabled))
);
}
stageUpdate(recordType, actionGroup = '', updates = {}) {
/* The updates arg is mutated by this method. */
/* eslint-disable no-param-reassign */
const {
config,
} = this.context;
const recordTypeConfig = get(config, ['recordTypes', recordType]);
const {
deletePermType,
lockable,
serviceConfig,
} = recordTypeConfig;
const {
documentName,
servicePath,
serviceType,
} = serviceConfig;
const resourceName = servicePath;
const shouldSetSoftDeletePerm = !deletePermType || deletePermType === 'soft' || deletePermType === 'all';
const shouldSetHardDeletePerm = deletePermType === 'hard' || deletePermType === 'all';
updates[resourceName] = shouldSetHardDeletePerm ? actionGroup : actionGroup.replace('D', '');
if (shouldSetSoftDeletePerm) {
const softDeleteResourceName = `/${resourceName}/*/workflow/delete`;
if (!actionGroup) {
updates[softDeleteResourceName] = '';
} else if (actionGroup.includes('D')) {
updates[softDeleteResourceName] = 'CRUDL';
} else {
updates[softDeleteResourceName] = 'RL';
}
}
if (lockable) {
const lockResourceName = `/${resourceName}/*/workflow/lock`;
if (!actionGroup) {
updates[lockResourceName] = '';
} else if (
actionGroup.includes('C')
|| actionGroup.includes('U')
|| actionGroup.includes('D')
) {
updates[lockResourceName] = 'CRUDL';
} else {
updates[lockResourceName] = 'RL';
}
}
if (serviceType === 'authority') {
// Permissions on authorities should be set on both the authorities and their items.
const itemResourceName = documentName;
updates[itemResourceName] = actionGroup;
}
if (resourceName === 'authorization/roles') {
// If the authorization/roles resource can be read, allow permissions to be read and
// listed. This allows the permissions list to be displayed when viewing the role.
const permissionsResourceName = 'authorization/permissions';
updates[permissionsResourceName] = actionGroup && actionGroup.includes('R') ? 'RL' : '';
}
// Always allow read and list on servicegroups.
updates.servicegroups = 'RL';
return updates;
/* eslint-enable no-param-reassign */
}
updatePerms(updates) {
const perms = this.getPermsMap() || {};
Object.assign(perms, updates);
return Immutable.List(
Object.keys(perms)
.filter((resourceName) => !!perms[resourceName])
.map((resourceName) => Immutable.Map({
resourceName,
actionGroup: perms[resourceName],
})),
);
}
renderHeaderButton(serviceType, actionGroup) {
const {
readOnly,
} = this.props;
return (
<MiniButton
autoWidth
onClick={this.handleHeaderButtonClick}
data-servicetype={serviceType}
data-actiongroup={actionGroup}
disabled={readOnly}
>
<FormattedMessage {...permMessages[actionGroup]} />
</MiniButton>
);
}
renderRadioButton(perms, recordType, resourceName, value) {
const {
readOnly,
} = this.props;
const className = readOnly ? permissionButtonStyles.readOnly : permissionButtonStyles.normal;
let checked = false;
if (perms) {
let effectivePerms = perms[resourceName];
if (effectivePerms === 'CRUL') {
// Check for soft-delete perm, and synthesize a delete perm if present.
const workflowDeleteResourceName = `/${resourceName}/*/workflow/delete`;
if (perms[workflowDeleteResourceName] === 'CRUDL') {
effectivePerms = 'CRUDL';
}
}
checked = value ? effectivePerms === value : !effectivePerms;
}
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label className={className}>
<FormattedMessage {...permMessages[value]} />
<input
checked={checked}
data-name={recordType}
type="radio"
value={value}
disabled={readOnly}
onChange={this.handleRadioChange}
/>
<div />
</label>
);
}
renderPermRows() {
const {
intl,
} = this.context;
const perms = this.getPermsMap() || {};
const sections = [];
serviceTypes.forEach((serviceType) => {
const rows = [];
this.getRecordTypeConfigs()
.filter((recordTypeConfig) => recordTypeConfig.serviceConfig.serviceType === serviceType)
.sort((recordTypeConfigA, recordTypeConfigB) => {
// Primary sort by sortOrder
let sortOrderA = recordTypeConfigA.sortOrder;
let sortOrderB = recordTypeConfigB.sortOrder;
if (typeof sortOrderA !== 'number') {
sortOrderA = Number.MAX_VALUE;
}
if (typeof sortOrderB !== 'number') {
sortOrderB = Number.MAX_VALUE;
}
if (sortOrderA !== sortOrderB) {
return (sortOrderA > sortOrderB ? 1 : -1);
}
// Secondary sort by label
const labelA = intl.formatMessage(recordTypeConfigA.messages.record.collectionName);
const labelB = intl.formatMessage(recordTypeConfigB.messages.record.collectionName);
// FIXME: This should be locale aware
return labelA.localeCompare(labelB);
})
.forEach((recordTypeConfig) => {
const { name } = recordTypeConfig;
const resourceName = recordTypeConfig.serviceConfig.servicePath;
const nameMessage = get(recordTypeConfig, ['messages', 'record', 'collectionName']);
const formattedName = nameMessage
? intl.formatMessage(nameMessage)
: `[ ${name} ]`;
rows.push(
<div key={name}>
<div>{formattedName}</div>
<div>
{this.renderRadioButton(perms, name, resourceName, '')}
{this.renderRadioButton(perms, name, resourceName, 'RL')}
{this.renderRadioButton(perms, name, resourceName, 'CRUL')}
{this.renderRadioButton(perms, name, resourceName, 'CRUDL')}
</div>
</div>,
);
});
sections.push(
<section key={serviceType}>
<header>
<h3><FormattedMessage {...serviceTypeMessages[serviceType]} /></h3>
<ul>
<li>{this.renderHeaderButton(serviceType, '')}</li>
<li>{this.renderHeaderButton(serviceType, 'RL')}</li>
<li>{this.renderHeaderButton(serviceType, 'CRUL')}</li>
<li>{this.renderHeaderButton(serviceType, 'CRUDL')}</li>
</ul>
</header>
{rows}
</section>,
);
});
return sections;
}
render() {
const {
readOnly,
resourceNames,
} = this.props;
if (!resourceNames) {
return null;
}
let {
value,
} = this.props;
if (value && !Immutable.List.isList(value)) {
value = Immutable.List.of(value);
}
const className = readOnly ? styles.readOnly : styles.common;
return (
<div className={className}>
{this.renderPermRows()}
</div>
);
}
}
PermissionsInput.propTypes = propTypes;
PermissionsInput.contextTypes = contextTypes;