Commit 4551505c authored by Martin Kraft's avatar Martin Kraft Committed by Jesús Espino
Browse files

MM-10693: Adds new UI to update post edit time limit. (#1269)

parent b5439846
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import {Constants} from 'utils/constants';
export default class EditPostTimeLimitButton extends React.Component {
static propTypes = {
timeLimit: PropTypes.number.isRequired,
onClick: PropTypes.func,
};
render = () => {
let messageID;
if (this.props.timeLimit === Constants.UNSET_POST_EDIT_TIME_LIMIT) {
messageID = 'edit_post.time_limit_button.no_limit';
} else {
messageID = 'edit_post.time_limit_button.for_n_seconds';
}
return (
<button
className='edit-post-time-limit-button'
onClick={this.props.onClick}
>
<FormattedMessage
id={messageID}
values={{n: this.props.timeLimit}}
/>
</button>
);
};
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import EditPostTimeLimitButton from './edit_post_time_limit_button';
function mapStateToProps(state, ownProps) {
const {PostEditTimeLimit} = getConfig(state);
return {
...ownProps,
timeLimit: parseInt(PostEditTimeLimit, 10),
};
}
export default connect(mapStateToProps)(EditPostTimeLimitButton);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import {Constants} from 'utils/constants';
import {localizeMessage} from 'utils/utils.jsx';
const INT32_MAX = 2147483647;
export default class EditPostTimeLimitModal extends React.Component {
static propTypes = {
config: PropTypes.object.isRequired,
show: PropTypes.bool,
onClose: PropTypes.func.isRequired,
actions: PropTypes.shape({
updateConfig: PropTypes.func.isRequired,
getConfig: PropTypes.func.isRequired,
}).isRequired,
};
constructor(props) {
super(props);
this.state = {
postEditTimeLimit: parseInt(props.config.ServiceSettings.PostEditTimeLimit, 10),
saving: false,
errorMessage: '',
};
}
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this.props.actions.getConfig();
}
save = async () => {
this.setState({saving: true, errorMessage: ''});
const val = parseInt(this.state.postEditTimeLimit, 10);
if (val !== Constants.UNSET_POST_EDIT_TIME_LIMIT) {
if (val.toString() === 'NaN' || val <= 0 || val > INT32_MAX) {
this.setState({errorMessage: localizeMessage('edit_post.time_limit_modal.invalid_time_limit', 'Invalid time limit'), saving: false});
return false;
}
}
const newConfig = JSON.parse(JSON.stringify(this.props.config));
newConfig.ServiceSettings.PostEditTimeLimit = val;
const {error: err} = await this.props.actions.updateConfig(newConfig);
if (err) {
this.setState({errorMessage: err, saving: false});
} else {
this.setState({saving: false});
this.props.onClose();
}
return true;
}
handleOptionChange = (e) => {
const {value} = e.target;
if (value === Constants.ALLOW_EDIT_POST_ALWAYS) {
this.setState({postEditTimeLimit: Constants.UNSET_POST_EDIT_TIME_LIMIT});
} else {
this.setState({postEditTimeLimit: ''});
}
}
handleSecondsChange = (e) => {
const {value} = e.target;
this.setState({postEditTimeLimit: value});
}
render = () => {
return (
<Modal
dialogClassName='admin-modal edit-post-time-limit-modal'
show={this.props.show}
>
<Modal.Header
closeButton={true}
>
<h4 className='modal-title'>
<FormattedMessage
id='edit_post.time_limit_modal.title'
defaultMessage='Configure Global Edit Post Time Limit'
/>
</h4>
</Modal.Header>
<Modal.Body>
<FormattedMessage
id='edit_post.time_limit_modal.description'
defaultMessage='Setting a time limit applies to all users who have the "Edit Post" permissions in any permission scheme.'
/>
<div>
<input
id='anytime'
type='radio'
name='limit'
value={Constants.ALLOW_EDIT_POST_ALWAYS}
checked={this.state.postEditTimeLimit === Constants.UNSET_POST_EDIT_TIME_LIMIT}
onChange={this.handleOptionChange}
/>
<label htmlFor='anytime'>
<FormattedMessage
id='edit_post.time_limit_modal.option_label_anytime'
defaultMessage='Anytime'
/>
</label>
</div>
<div>
<input
id='timelimit'
type='radio'
name='limit'
value={Constants.ALLOW_EDIT_POST_TIME_LIMIT}
checked={this.state.postEditTimeLimit !== Constants.UNSET_POST_EDIT_TIME_LIMIT}
onChange={this.handleOptionChange}
/>
<label htmlFor='timelimit'>
<FormattedMessage
id='edit_post.time_limit_modal.option_label_time_limit.preinput'
defaultMessage='Can edit for'
/>
</label>
<input
type='number'
min='0'
step='1'
max={INT32_MAX}
id='editPostTimeLimit'
readOnly={this.state.postEditTimeLimit === Constants.UNSET_POST_EDIT_TIME_LIMIT}
onChange={this.handleSecondsChange}
value={this.state.postEditTimeLimit === Constants.UNSET_POST_EDIT_TIME_LIMIT ? '' : this.state.postEditTimeLimit}
/>
<label htmlFor='timelimit'>
<FormattedMessage
id='edit_post.time_limit_modal.option_label_time_limit.postinput'
defaultMessage='seconds after posting'
/>
</label>
</div>
<FormattedMessage
id='edit_post.time_limit_modal.subscript'
defaultMessage='Set the length of time users have to edit their messages after posting.'
/>
</Modal.Body>
<Modal.Footer>
<div className='edit-post-time-limit-modal__error'>
{this.state.errorMessage}
</div>
<button
type='button'
className='btn btn-cancel'
onClick={this.props.onClose}
>
<FormattedMessage
id='confirm_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<button
id='linkModalCloseButton'
type='button'
className='btn btn-default'
onClick={this.save}
disabled={this.state.saving}
>
<FormattedMessage
id={this.state.saving ? 'save_button.saving' : 'edit_post.time_limit_modal.save_button'}
defaultMessage='Save Edit Time'
/>
</button>
</Modal.Footer>
</Modal>
);
};
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getConfig, updateConfig} from 'mattermost-redux/actions/admin';
import * as Selectors from 'mattermost-redux/selectors/entities/admin';
import EditPostTimeLimitModal from './edit_post_time_limit_modal';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: Selectors.getConfig(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
updateConfig,
getConfig,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EditPostTimeLimitModal);
......@@ -16,6 +16,7 @@ export class PermissionDescription extends React.Component {
rowType: PropTypes.string.isRequired,
inherited: PropTypes.object,
selectRow: PropTypes.func.isRequired,
additionalValues: PropTypes.object,
};
constructor(props) {
......@@ -38,7 +39,10 @@ export class PermissionDescription extends React.Component {
}
parentPermissionClicked = (e) => {
if (e.target.tagName === 'A') {
const isInheritLink = e.target.parentElement.parentElement.className === 'inherit-link-wrapper';
if (e.target.parentElement.className !== 'permission-description' && !isInheritLink) {
e.stopPropagation();
} else if (isInheritLink) {
this.props.selectRow(this.props.id);
e.stopPropagation();
}
......@@ -50,18 +54,40 @@ export class PermissionDescription extends React.Component {
let content = '';
if (inherited) {
content = (
<FormattedHTMLMessage
id='admin.permissions.inherited_from'
values={{
name: this.props.intl.formatMessage({
id: 'admin.permissions.roles.' + inherited.name + '.name',
defaultMessage: inherited.display_name,
}),
}}
/>
<span className='inherit-link-wrapper'>
<FormattedHTMLMessage
id='admin.permissions.inherited_from'
values={{
name: this.props.intl.formatMessage({
id: 'admin.permissions.roles.' + inherited.name + '.name',
defaultMessage: inherited.display_name,
}),
}}
/>
</span>
);
} else {
content = <FormattedMessage id={'admin.permissions.' + rowType + '.' + id + '.description'}/>;
content = (
<FormattedMessage
id={'admin.permissions.' + rowType + '.' + id + '.description'}
values={this.props.additionalValues}
/>
);
}
let tooltip = (
<Overlay
show={this.state.open}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
target={this.refs.content}
>
<Tooltip id={this.id}>
{content}
</Tooltip>
</Overlay>
);
if (content.props.values && Object.keys(content.props.values).length > 0) {
tooltip = null;
}
content = (
<span
......@@ -72,16 +98,7 @@ export class PermissionDescription extends React.Component {
onMouseOut={this.closeTooltip}
>
{content}
<Overlay
show={this.state.open}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
target={this.refs.content}
>
<Tooltip id={this.id}>
{content}
</Tooltip>
</Overlay>
{tooltip}
</span>
);
......
......@@ -24,6 +24,7 @@ export default class PermissionGroup extends React.Component {
selectRow: PropTypes.func.isRequired,
root: PropTypes.bool,
onChange: PropTypes.func.isRequired,
additionalValues: PropTypes.object,
};
constructor(props) {
......@@ -34,7 +35,7 @@ export default class PermissionGroup extends React.Component {
};
}
componentWillUpdate(nextProps) {
UNSAFE_componentWillUpdate(nextProps) { // eslint-disable-line camelcase
if (this.props.selected !== nextProps.selected) {
if (this.getRecursivePermissions(this.props.permissions).indexOf(nextProps.selected) !== -1) {
this.setState({expanded: true});
......@@ -128,7 +129,7 @@ export default class PermissionGroup extends React.Component {
return true;
}
renderPermission = (permission) => {
renderPermission = (permission, additionalValues) => {
if (!this.isInScope(permission)) {
return null;
}
......@@ -144,6 +145,7 @@ export default class PermissionGroup extends React.Component {
inherited={comesFromParent ? this.props.parentRole : null}
value={active ? 'checked' : ''}
onChange={this.toggleSelectRow}
additionalValues={additionalValues}
/>
);
}
......@@ -157,6 +159,7 @@ export default class PermissionGroup extends React.Component {
selectRow={this.props.selectRow}
readOnly={this.props.readOnly}
permissions={g.permissions}
additionalValues={this.props.additionalValues}
role={this.props.role}
parentRole={this.props.parentRole}
scope={this.props.scope}
......@@ -223,13 +226,14 @@ export default class PermissionGroup extends React.Component {
}
render = () => {
const {id, permissions, readOnly, combined, root, selected} = this.props;
const {id, permissions, readOnly, combined, root, selected, additionalValues} = this.props;
if (!this.hasPermissionsOnScope()) {
return null;
}
const permissionsRows = permissions.map((group) => {
if (typeof group === 'string') {
return this.renderPermission(group);
const addVals = additionalValues && additionalValues[group] ? additionalValues[group] : {};
return this.renderPermission(group, addVals);
}
return this.renderGroup(group);
});
......
......@@ -17,6 +17,7 @@ export default class PermissionRow extends React.Component {
selectRow: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
additionalValues: PropTypes.object,
};
toggleSelect = () => {
......@@ -27,7 +28,7 @@ export default class PermissionRow extends React.Component {
}
render = () => {
const {id, inherited, value, readOnly, selected} = this.props;
const {id, inherited, value, readOnly, selected, additionalValues} = this.props;
let classes = 'permission-row';
if (readOnly) {
classes += ' read-only';
......@@ -44,13 +45,16 @@ export default class PermissionRow extends React.Component {
>
<PermissionCheckbox value={value}/>
<span className='permission-name'>
<FormattedMessage id={'admin.permissions.permission.' + id + '.name'}/>
<FormattedMessage
id={'admin.permissions.permission.' + id + '.name'}
/>
</span>
<PermissionDescription
inherited={inherited}
id={id}
selectRow={this.props.selectRow}
rowType='permission'
additionalValues={additionalValues}
/>
</div>
);
......
......@@ -7,6 +7,9 @@ import {FormattedMessage} from 'react-intl';
import PermissionGroup from './permission_group.jsx';
import EditPostTimeLimitButton from './edit_post_time_limit_button';
import EditPostTimeLimitModal from './edit_post_time_limit_modal';
const GROUPS = [
{
id: 'teams',
......@@ -90,6 +93,22 @@ export default class PermissionsTree extends React.Component {
},
};
onClickEditPostTimeLimitButton = () => {
this.setState({editTimeLimitModalIsVisible: true});
}
constructor() {
super();
this.state = {
editTimeLimitModalIsVisible: false,
};
this.ADDITIONAL_VALUES = {
edit_post: {
editTimeLimitButton: <EditPostTimeLimitButton onClick={() => this.onClickEditPostTimeLimitButton()}/>,
},
};
}
toggleGroup = (ids) => {
if (this.props.readOnly) {
return;
......@@ -122,6 +141,7 @@ export default class PermissionsTree extends React.Component {
selectRow={this.props.selectRow}
readOnly={this.props.readOnly}
permissions={GROUPS}
additionalValues={this.ADDITIONAL_VALUES}
role={this.props.role}
parentRole={this.props.parentRole}
scope={this.props.scope}
......@@ -130,6 +150,10 @@ export default class PermissionsTree extends React.Component {
root={true}
/>
</div>
<EditPostTimeLimitModal
onClose={() => this.setState({editTimeLimitModalIsVisible: false})}
show={this.state.editTimeLimitModalIsVisible}
/>
</div>
);
};
......
......@@ -218,7 +218,7 @@
"admin.permissions.permission.manage_oauth.name": "Manage OAuth Applications",
"admin.permissions.permission.manage_oauth.description": "Create, edit and delete OAuth 2.0 application tokens.",
"admin.permissions.permission.edit_post.name": "Edit Posts",
"admin.permissions.permission.edit_post.description": "Anytime after posting, allow users to edit their own posts.",
"admin.permissions.permission.edit_post.description": "{editTimeLimitButton} after posting, allow users to edit their own posts.",
"admin.permissions.permission.delete_post.name": "Delete Own Posts",
"admin.permissions.permission.delete_post.description": "Authors own posts can be deleted.",
"admin.permissions.permission.delete_others_posts.name": "Delete Any Post",
......@@ -1753,6 +1753,16 @@
"edit_post.edit": "Edit {title}",
"edit_post.editPost": "Edit the post...",
"edit_post.save": "Save",
"edit_post.time_limit_button.no_limit": "⚙ Anytime",
"edit_post.time_limit_button.for_n_seconds": "⚙ For {n} seconds",
"edit_post.time_limit_modal.title": "Configure Global Edit Post Time Limit",
"edit_post.time_limit_modal.description": "Setting a time limit applies to all users who have the \"Edit Post\" permissions in any permission scheme.",
"edit_post.time_limit_modal.option_label_anytime": "Anytime",
"edit_post.time_limit_modal.option_label_time_limit.preinput": "Can edit for",
"edit_post.time_limit_modal.option_label_time_limit.postinput": "seconds after posting",
"edit_post.time_limit_modal.subscript": "Set the length of time users have to edit their messages after posting.",
"edit_post.time_limit_modal.save_button": "Save Edit Time",
"edit_post.time_limit_modal.invalid_time_limit": "Invalid time limit",
"email_signup.address": "Email Address",
"email_signup.createTeam": "Create Team",
"email_signup.emailError": "Please enter a valid email address.",
......@@ -3355,4 +3365,4 @@
"webrtc.unpause_video": "Turn on camera",
"webrtc.unsupported": "{username} client does not support video calls.",
"youtube_video.notFound": "Video not found"
}
}
\ No newline at end of file
......@@ -10869,7 +10869,7 @@
"dev": true
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#65871294964d427e3c4e59860a0fd0c369317709",
"version": "github:mattermost/mattermost-redux#15261d20e735bc09b0e8ef64a66b3fe4996715e5",
"from": "github:mattermost/mattermost-redux#advanced-permissions-phase-2",
"requires": {
"deep-equal": "1.0.1",
......
.edit-post-time-limit-button {
border-radius: 4px;
border-color: #333;
span {
display: inline !important;
}
}
\ No newline at end of file
.edit-post-time-limit-modal {
label {
margin-left: 5px;
margin-right: 5px;
}
.edit-post-time-limit-modal__error {
display: inline-block;
width: 65%;