Commit fe92c23c authored by Siyuan Liu's avatar Siyuan Liu Committed by Saturnino Abril
Browse files

#7775: allow team / system admin to convert a channel to private (#418)

* allow team / system admin to convert a channel to private

* rebase and fix merge conflicts

* update PR

* add channel conversion  to ChannelPermissionGate

* change permission to system admins only

* use convertChannelToPrivate instead of updateChannel action

* rebase to latest and fix failing test on master

* reverted fix on master
parent 10c0de5a
......@@ -27,6 +27,7 @@ import ChannelInfoModal from 'components/channel_info_modal';
import ChannelInviteModal from 'components/channel_invite_modal';
import ChannelMembersModal from 'components/channel_members_modal';
import ChannelNotificationsModal from 'components/channel_notifications_modal';
import ConvertChannelModal from 'components/convert_channel_modal';
import DeleteChannelModal from 'components/delete_channel_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
......@@ -41,6 +42,7 @@ import PinIcon from 'components/svg/pin_icon';
import SearchIcon from 'components/svg/search_icon';
import ToggleModalButtonRedux from 'components/toggle_modal_button_redux';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import Pluggable from 'plugins/pluggable';
......@@ -729,6 +731,36 @@ export default class ChannelHeader extends React.Component {
);
}
if (!this.props.isDefault && channel.type === Constants.OPEN_CHANNEL) {
dropdownContents.push(
<SystemPermissionGate
permissions={[Permissions.MANAGE_SYSTEM]}
key='convert_channel_permission'
>
<li
key='convert_channel'
role='presentation'
>
<ToggleModalButtonRedux
id='channelConvert'
role='menuitem'
modalId={ModalIdentifiers.CONVERT_CHANNEL}
dialogType={ConvertChannelModal}
dialogProps={{
channelId: channel.id,
channelDisplayName: channel.display_name,
}}
>
<FormattedMessage
id='channel_header.convert'
defaultMessage='Convert to Private Channel'
/>
</ToggleModalButtonRedux>
</li>
</SystemPermissionGate>
);
}
if (!this.props.isDefault) {
dropdownContents.push(
<ChannelPermissionGate
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
import Constants from 'utils/constants.jsx';
export default class ConvertChannelModal extends React.PureComponent {
static propTypes = {
/**
* Function called when modal is dismissed
*/
onHide: PropTypes.func.isRequired,
channelId: PropTypes.string.isRequired,
channelDisplayName: PropTypes.string.isRequired,
actions: PropTypes.shape({
/**
* Function called for converting channel to private,
*/
convertChannelToPrivate: PropTypes.func.isRequired,
}),
}
constructor(props) {
super(props);
this.state = {show: true};
}
handleConvert = () => {
const {actions, channelId} = this.props;
if (channelId.length !== Constants.CHANNEL_ID_LENGTH) {
return;
}
actions.convertChannelToPrivate(channelId);
trackEvent('actions', 'convert_to_private_channel', {channel_id: channelId});
this.onHide();
}
onHide = () => {
this.setState({show: false});
}
render() {
const {
channelDisplayName,
onHide,
} = this.props;
return (
<Modal
show={this.state.show}
onHide={this.onHide}
onExited={onHide}
>
<Modal.Header closeButton={true}>
<h4 className='modal-title'>
<FormattedMessage
id='convert_channel.title'
defaultMessage='Convert {display_name} to a private channel?'
values={{
display_name: channelDisplayName,
}}
/>
</h4>
</Modal.Header>
<Modal.Body>
<p>
<FormattedHTMLMessage
id='convert_channel.question1'
defaultMessage='When you convert <strong>{display_name}</strong> to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.'
values={{
display_name: channelDisplayName,
}}
/>
</p>
<p>
<FormattedHTMLMessage
id='convert_channel.question2'
defaultMessage='The change is permanent and cannot be undone.'
/>
</p>
<p>
<FormattedHTMLMessage
id='convert_channel.question3'
defaultMessage='Are you sure you want to convert <strong>{display_name}</strong> to a private channel?'
values={{
display_name: channelDisplayName,
}}
/>
</p>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.onHide}
tabIndex='2'
>
<FormattedMessage
id='convert_channel.cancel'
defaultMessage='No, cancel'
/>
</button>
<button
type='button'
className='btn btn-primary'
data-dismiss='modal'
onClick={this.handleConvert}
autoFocus={true}
tabIndex='1'
>
<FormattedMessage
id='convert_channel.confirm'
defaultMessage='Yes, convert to private channel'
/>
</button>
</Modal.Footer>
</Modal>
);
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {convertChannelToPrivate} from 'mattermost-redux/actions/channels';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import ConvertChannelModal from './convert_channel_modal.jsx';
function mapStateToProps(state) {
return {
currentTeamDetails: getCurrentTeam(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
convertChannelToPrivate,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ConvertChannelModal);
......@@ -22,6 +22,8 @@ import WebrtcStore from 'stores/webrtc_store.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import {ActionTypes, Constants, ModalIdentifiers, RHSStates, UserStatuses} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import ConvertChannelModal from 'components/convert_channel_modal';
import ChannelInfoModal from 'components/channel_info_modal';
import ChannelInviteModal from 'components/channel_invite_modal';
import ChannelMembersModal from 'components/channel_members_modal';
......@@ -37,6 +39,7 @@ import SearchIcon from 'components/svg/search_icon';
import ToggleModalButton from 'components/toggle_modal_button.jsx';
import ToggleModalButtonRedux from 'components/toggle_modal_button_redux';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import Pluggable from 'plugins/pluggable';
......@@ -362,6 +365,7 @@ export default class Navbar extends React.Component {
let setChannelPurposeOption;
let notificationPreferenceOption;
let renameChannelOption;
let convertChannelOption;
let deleteChannelOption;
let leaveChannelOption;
......@@ -598,6 +602,28 @@ export default class Navbar extends React.Component {
</ChannelPermissionGate>
);
if (!ChannelStore.isDefault(channel) && channel.type === Constants.OPEN_CHANNEL) {
convertChannelOption = (
<SystemPermissionGate permissions={[Permissions.MANAGE_SYSTEM]}>
<li role='presentation'>
<ToggleModalButton
role='menuitem'
dialogType={ConvertChannelModal}
dialogProps={{
channelId: channel.id,
channelDisplayName: channel.display_name,
}}
>
<FormattedMessage
id='channel_header.convert'
defaultMessage='Convert to Private Channel'
/>
</ToggleModalButton>
</li>
</SystemPermissionGate>
);
}
renameChannelOption = (
<ChannelPermissionGate
channelId={channel.id}
......@@ -709,6 +735,7 @@ export default class Navbar extends React.Component {
{setChannelHeaderOption}
{setChannelPurposeOption}
{renameChannelOption}
{convertChannelOption}
{deleteChannelOption}
{leaveChannelOption}
{toggleFavoriteOption}
......
......@@ -213,6 +213,24 @@ function renderDisplayNameChangeMessage(post, options) {
);
}
function renderConvertChannelToPrivateMessage(post, options) {
if (!(post.props.username)) {
return null;
}
const username = renderUsernameForUserIdAndUsername(post.user_id, post.props.username, options);
return (
<FormattedMessage
id='api.channel.post_convert_channel_to_private.updated_from'
defaultMessage='{username} converted the channel from public to private'
values={{
username,
}}
/>
);
}
function renderPurposeChangeMessage(post, options) {
if (!post.props.username) {
return null;
......@@ -290,6 +308,7 @@ const systemMessageRenderers = {
[PostTypes.REMOVE_FROM_TEAM]: renderRemoveFromTeamMessage,
[PostTypes.HEADER_CHANGE]: renderHeaderChangeMessage,
[PostTypes.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage,
[PostTypes.CONVERT_CHANNEL]: renderConvertChannelToPrivateMessage,
[PostTypes.PURPOSE_CHANGE]: renderPurposeChangeMessage,
[PostTypes.CHANNEL_DELETED]: renderChannelDeletedMessage,
};
......
......@@ -1252,6 +1252,7 @@
"api.channel.delete_channel.archived": "{username} archived the channel.",
"api.channel.join_channel.post_and_forget": "{username} joined the channel.",
"api.channel.leave.left": "{username} left the channel.",
"api.channel.post_convert_channel_to_private.updated_from": "{username} converted the channel from public to private",
"api.channel.post_update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {old} to: {new}",
"api.channel.post_update_channel_header_message_and_forget.removed": "{username} removed the channel header (was: {old})",
"api.channel.post_update_channel_header_message_and_forget.updated_from": "{username} updated the channel header from: {old} to: {new}",
......@@ -1356,6 +1357,7 @@
"channel_header.addToFavorites": "Add to Favorites",
"channel_header.channelHeader": "Edit Channel Header",
"channel_header.channelMembers": "Members",
"channel_header.convert": "Convert to Private Channel",
"channel_header.delete": "Delete Channel",
"channel_header.directchannel.you": "{displayname} (you) ",
"channel_header.flagged": "Flagged Posts",
......@@ -1487,6 +1489,12 @@
"confirm_modal.cancel": "Cancel",
"connecting_screen": "Connecting",
"copy_url_context_menu.getChannelLink": "Copy Link",
"convert_channel.cancel": "No, cancel",
"convert_channel.confirm": "Yes, convert to private channel",
"convert_channel.title": "Convert {display_name} to a private channel?",
"convert_channel.question1": "When you convert <strong>{display_name}</strong> to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.",
"convert_channel.question2": "The change is permanent and cannot be undone.",
"convert_channel.question3": "Are you sure you want to convert <strong>{display_name}</strong> to a private channel?",
"create_comment.addComment": "Add a comment...",
"create_comment.comment": "Add Comment",
"create_comment.commentLength": "Comment length must be less than {max} characters.",
......
......@@ -740,6 +740,35 @@ exports[`components/navbar/Navbar should match snapshot, if WebRTC is not enable
</a>
</li>
</Connect(ChannelPermissionGate)>
<Connect(SystemPermissionGate)
permissions={
Array [
"manage_system",
]
}
>
<li
role="presentation"
>
<ModalToggleButton
className=""
dialogProps={
Object {
"channelDisplayName": "display_name",
"channelId": "channel_id",
}
}
dialogType={[Function]}
role="menuitem"
>
<FormattedMessage
defaultMessage="Convert to Private Channel"
id="channel_header.convert"
values={Object {}}
/>
</ModalToggleButton>
</li>
</Connect(SystemPermissionGate)>
<Connect(ChannelPermissionGate)
channelId="channel_id"
permissions={
......@@ -1356,6 +1385,35 @@ exports[`components/navbar/Navbar should match snapshot, if not licensed 1`] = `
</a>
</li>
</Connect(ChannelPermissionGate)>
<Connect(SystemPermissionGate)
permissions={
Array [
"manage_system",
]
}
>
<li
role="presentation"
>
<ModalToggleButton
className=""
dialogProps={
Object {
"channelDisplayName": "display_name",
"channelId": "channel_id",
}
}
dialogType={[Function]}
role="menuitem"
>
<FormattedMessage
defaultMessage="Convert to Private Channel"
id="channel_header.convert"
values={Object {}}
/>
</ModalToggleButton>
</li>
</Connect(SystemPermissionGate)>
<Connect(ChannelPermissionGate)
channelId="channel_id"
permissions={
......@@ -1788,6 +1846,35 @@ exports[`components/navbar/Navbar should match snapshot, valid state 1`] = `
</a>
</li>
</Connect(ChannelPermissionGate)>
<Connect(SystemPermissionGate)
permissions={
Array [
"manage_system",
]
}
>
<li
role="presentation"
>
<ModalToggleButton
className=""
dialogProps={
Object {
"channelDisplayName": "display_name",
"channelId": "channel_id",
}
}
dialogType={[Function]}
role="menuitem"
>
<FormattedMessage
defaultMessage="Convert to Private Channel"
id="channel_header.convert"
values={Object {}}
/>
</ModalToggleButton>
</li>
</Connect(SystemPermissionGate)>
<Connect(ChannelPermissionGate)
channelId="channel_id"
permissions={
......
......@@ -286,6 +286,7 @@ export const ModalIdentifiers = {
CREATE_DM_CHANNEL: 'create_dm_channel',
EDIT_CHANNEL_HEADER: 'edit_channel_header',
DELETE_POST: 'delete_post',
CONVERT_CHANNEL: 'convert_channel',
};
export const UserStatuses = {
......@@ -363,6 +364,7 @@ export const PostTypes = {
REMOVE_FROM_TEAM: 'system_remove_from_team',
HEADER_CHANGE: 'system_header_change',
DISPLAYNAME_CHANGE: 'system_displayname_change',
CONVERT_CHANNEL: 'system_convert_channel',
PURPOSE_CHANGE: 'system_purpose_change',
CHANNEL_DELETED: 'system_channel_deleted',
FAKE_PARENT_DELETED: 'system_fake_parent_deleted',
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment