Manage member permissions (#223)

* Manage member permissions

* Review feedback
This commit is contained in:
Chris Duarte 2017-02-08 13:01:17 -08:00 committed by Harrison Healey
parent 088a2c7737
commit 8c0f5c70d7
12 changed files with 294 additions and 21 deletions

View file

@ -0,0 +1,16 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {removeChannelMember} from 'service/actions/channels';
export function handleRemoveChannelMembers(teamId, channelId, members) {
return async (dispatch, getState) => {
try {
const requests = members.map((m) => dispatch(removeChannelMember(teamId, channelId, m, getState)));
await Promise.all(requests);
} catch (error) {
// should be handled by global error handling
}
};
}

View file

@ -102,7 +102,6 @@ export default class ChannelAddMembers extends PureComponent {
loadingMembers={this.props.loadMoreRequestStatus === 'started'}
selectable={true}
onRowSelect={this.handleRowSelect}
showSections={true}
/>
</View>
);

View file

@ -43,6 +43,7 @@ class ChannelInfo extends PureComponent {
currentChannelMemberCount: PropTypes.number,
isFavorite: PropTypes.bool.isRequired,
leaveChannelRequest: PropTypes.object.isRequired,
isAdmin: PropTypes.bool.isRequired,
actions: PropTypes.shape({
getChannelStats: PropTypes.func.isRequired,
goToChannelMembers: PropTypes.func.isRequired,
@ -129,7 +130,8 @@ class ChannelInfo extends PureComponent {
const {
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount
currentChannelMemberCount,
isAdmin
} = this.props;
return (
@ -166,23 +168,27 @@ class ChannelInfo extends PureComponent {
</View>
<ChannelInfoRow
action={this.props.actions.goToChannelMembers}
defaultMessage='Manage Members'
defaultMessage={isAdmin ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
textId='channel_header.manageMembers'
/>
<View style={style.separatorContainer}>
<View style={style.separator}/>
</View>
<ChannelInfoRow
action={this.props.actions.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'
textId={isAdmin ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
/>
<View style={style.separatorContainer}>
<View style={style.separator}/>
</View>
{isAdmin &&
<View>
<ChannelInfoRow
action={this.props.actions.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'
/>
<View style={style.separatorContainer}>
<View style={style.separator}/>
</View>
</View>
}
<ChannelInfoRow
action={() => this.handleLeave()}
defaultMessage='Leave Channel'

View file

@ -9,7 +9,8 @@ import {goToChannelMembers, goToChannelAddMembers, goBack} from 'app/actions/nav
import {getChannelStats} from 'service/actions/channels';
import {markFavorite, unmarkFavorite, leaveChannel} from 'app/actions/views/channel';
import {getCurrentChannel, getCurrentChannelStats, getChannelsByCategory} from 'service/selectors/entities/channels';
import {getUser} from 'service/selectors/entities/users';
import {getTheme} from 'service/selectors/entities/preferences';
import {getUser, getCurrentUserRoles} from 'service/selectors/entities/users';
import ChannelInfo from './channel_info';
@ -21,6 +22,9 @@ function mapStateToProps(state, ownProps) {
const favoriteChannels = getChannelsByCategory(state).favoriteChannels.map((f) => f.id);
const isFavorite = favoriteChannels.indexOf(currentChannel.id) > -1;
const leaveChannelRequest = state.requests.channels.leaveChannel;
const currentUserRoles = getCurrentUserRoles(state);
const isAdmin = currentUserRoles.includes('_admin');
return {
...ownProps,
@ -28,7 +32,9 @@ function mapStateToProps(state, ownProps) {
currentChannelCreatorName,
currentChannelMemberCount,
isFavorite,
leaveChannelRequest
leaveChannelRequest,
theme: getTheme(state),
isAdmin
};
}

View file

@ -3,44 +3,137 @@
import React, {PropTypes, PureComponent} from 'react';
import {
Alert,
InteractionManager,
StyleSheet,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MemberList from 'app/components/member_list';
import ChannelMembersTitle from './channel_members_title';
import RemoveMemberButton from './remove_member_button';
const style = StyleSheet.create({
container: {
flex: 1
}
});
export default class ChannelMembers extends PureComponent {
class ChannelMembers extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
currentChannel: PropTypes.object,
currentChannelMembers: PropTypes.array.isRequired,
currentChannelMemberCount: PropTypes.number.isRequired,
currentTeam: PropTypes.object,
preferences: PropTypes.object,
requestStatus: PropTypes.string,
isAdmin: PropTypes.bool.isRequired,
subscribeToHeaderEvent: React.PropTypes.func,
unsubscribeFromHeaderEvent: React.PropTypes.func,
actions: PropTypes.shape({
getProfilesInChannel: PropTypes.func.isRequired
getProfilesInChannel: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
handleRemoveChannelMembers: PropTypes.func.isRequired
})
}
static navigationProps = {
renderTitleComponent: () => {
return <ChannelMembersTitle/>;
},
renderRightComponent: (props, emitter) => {
return <RemoveMemberButton emitter={emitter}/>;
}
}
state = {
selectedMembers: {}
}
componentWillMount() {
this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress);
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, 0);
});
}
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('remove_members');
}
handleRemoveMembersPress = () => {
const {selectedMembers} = this.state;
const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]);
const {formatMessage} = this.props.intl;
if (!membersToRemove.length) {
Alert.alert(
formatMessage({
id: 'mobile.routes.channel_members.action',
defaultMessage: '{term} Members'
}, {
term: 'Remove'
}),
formatMessage({
id: 'mobile.routes.channel_members.action_message',
defaultMessage: 'You must select at least one member to {term} {prep} the channel.'
}, {
term: 'remove',
prep: 'from'
})
);
return;
}
Alert.alert(
formatMessage({
id: 'mobile.routes.channel_members.action',
defaultMessage: '{term} Members'
}, {
term: 'Remove'
}),
formatMessage({
id: 'mobile.routes.channel_members.action_message_confirm',
defaultMessage: 'Are you sure you want to {term} the selected members {prep} the channel?'
}, {
term: 'remove',
prep: 'from'
}),
[{
text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'})
}, {
text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}),
onPress: () => this.removeMembers(membersToRemove)
}]
);
}
removeMembers = (membersToRemove) => {
const {actions, currentTeam, currentChannel} = this.props;
actions.handleRemoveChannelMembers(currentTeam.id, currentChannel.id, membersToRemove).then(() => {
actions.goBack();
});
}
loadMoreMembers = () => {
if (this.props.requestStatus !== 'started' && this.props.currentChannelMembers.length < this.props.currentChannelMemberCount) {
this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.currentChannelMembers.length);
}
}
handleRowSelect = (id) => {
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
this.setState({
selectedMembers
});
}
render() {
return (
<View style={style.container}>
@ -49,8 +142,12 @@ export default class ChannelMembers extends PureComponent {
onListEndReached={this.loadMoreMembers}
preferences={this.props.preferences}
loadingMembers={this.props.requestStatus === 'started'}
selectable={this.props.isAdmin}
onRowSelect={this.handleRowSelect}
/>
</View>
);
}
}
export default injectIntl(ChannelMembers);

View file

@ -5,16 +5,21 @@ import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goBack} from 'app/actions/navigation';
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
import {getCurrentChannel, getCurrentChannelStats} from 'service/selectors/entities/channels';
import {getMyPreferences} from 'service/selectors/entities/preferences';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import {getProfilesInCurrentChannel} from 'service/selectors/entities/users';
import {getProfilesInCurrentChannel, getCurrentUserRoles} from 'service/selectors/entities/users';
import {getProfilesInChannel} from 'service/actions/users';
import ChannelMembers from './channel_members';
function mapStateToProps(state) {
const currentChannelMemberCount = getCurrentChannelStats(state) && getCurrentChannelStats(state).member_count;
const currentUserRoles = getCurrentUserRoles(state);
const isAdmin = currentUserRoles.includes('_admin');
return {
currentChannel: getCurrentChannel(state),
@ -22,14 +27,17 @@ function mapStateToProps(state) {
currentChannelMemberCount,
currentTeam: getCurrentTeam(state),
preferences: getMyPreferences(state),
requestStatus: state.requests.users.getProfilesInChannel.status
requestStatus: state.requests.users.getProfilesInChannel.status,
isAdmin
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfilesInChannel
getProfilesInChannel,
goBack,
handleRemoveChannelMembers
}, dispatch)
};
}

View file

@ -0,0 +1,47 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {
View
} from 'react-native';
import {getCurrentUserRoles} from 'service/selectors/entities/users';
import {getTheme} from 'service/selectors/entities/preferences';
import FormattedText from 'app/components/formatted_text';
function ChannelMemberTitle(props) {
return (
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, marginHorizontal: 50}}>
<FormattedText
id={props.isAdmin ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
defaultMessage={props.isAdmin ? 'Manage Members' : 'View Members'}
style={{color: props.theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold', textAlign: 'center'}}
/>
</View>
);
}
ChannelMemberTitle.propTypes = {
isAdmin: PropTypes.bool,
theme: PropTypes.object
};
ChannelMemberTitle.defaultProps = {
theme: {}
};
function mapStateToProps(state) {
const currentUserRoles = getCurrentUserRoles(state);
const isAdmin = currentUserRoles.includes('_admin');
return {
isAdmin,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelMemberTitle);

View file

@ -0,0 +1,57 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getCurrentUserRoles} from 'service/selectors/entities/users';
import {getTheme} from 'service/selectors/entities/preferences';
function RemoveMemberButton(props) {
if (!props.isAdmin) {
return null;
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={() => props.emitter('remove_members')}
style={{paddingHorizontal: 15}}
>
<FormattedText
id='channel_members_modal.remove'
defaultMessage='Remove'
style={{color: props.theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
</View>
);
}
RemoveMemberButton.propTypes = {
emitter: PropTypes.func.isRequired,
isAdmin: PropTypes.bool.isRequired,
theme: PropTypes.object
};
RemoveMemberButton.defaultProps = {
theme: {}
};
function mapStateToProps(state) {
const currentUserRoles = getCurrentUserRoles(state);
const isAdmin = currentUserRoles.includes('_admin');
return {
isAdmin,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(RemoveMemberButton);

View file

@ -1512,6 +1512,9 @@
"mobile.routes.channelInfo.createdBy": "Created by {creator} on ",
"mobile.routes.channelInfo.delete_channel": "Delete Channel",
"mobile.routes.channelInfo.favorite": "Favorite",
"mobile.routes.channel_members.action": "{term} Members",
"mobile.routes.channel_members.action_message": "You must select at least one member to {term} {prep} the channel.",
"mobile.routes.channel_members.action_message_confirm": "Are you sure you want to {term} the selected members {prep} the channel?",
"more_channels.close": "Close",
"more_channels.create": "Create New Channel",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",

View file

@ -17,6 +17,10 @@ export function getCurrentChannelId(state) {
return state.entities.channels.currentId;
}
export function getChannelMemberships(state) {
return state.entities.channels.myMembers;
}
export const getCurrentChannel = createSelector(
getAllChannels,
getCurrentChannelId,
@ -31,6 +35,14 @@ export const getCurrentChannel = createSelector(
}
);
export const getCurrentChannelMembership = createSelector(
getCurrentChannelId,
getChannelMemberships,
(currentChannelId, channelMemberships) => {
return channelMemberships[currentChannelId];
}
);
export const getCurrentChannelStats = createSelector(
getAllChannelStats,
getCurrentChannelId,

View file

@ -17,6 +17,10 @@ export function getTeamStats(state) {
return state.entities.teams.stats;
}
export function getTeamMemberships(state) {
return state.entities.teams.myMembers;
}
export const getCurrentTeam = createSelector(
getTeams,
getCurrentTeamId,
@ -25,6 +29,14 @@ export const getCurrentTeam = createSelector(
}
);
export const getCurrentTeamMembership = createSelector(
getCurrentTeamId,
getTeamMemberships,
(currentTeamId, teamMemberships) => {
return teamMemberships[currentTeamId];
}
);
export const getCurrentTeamUrl = createSelector(
getCurrentUrl,
getCurrentTeam,

View file

@ -3,7 +3,8 @@
import {createSelector} from 'reselect';
import {getCurrentChannelId} from './channels';
import {getCurrentChannelId, getCurrentChannelMembership} from './channels';
import {getCurrentTeamMembership} from './teams';
export function getCurrentUserId(state) {
return state.entities.users.currentId;
@ -37,6 +38,15 @@ export const getCurrentUser = createSelector(
}
);
export const getCurrentUserRoles = createSelector(
getCurrentChannelMembership,
getCurrentTeamMembership,
getCurrentUser,
(currentChannelMembership, currentTeamMembership, currentUser) => {
return `${currentTeamMembership.roles} ${currentChannelMembership.roles} ${currentUser.roles}`;
}
);
export const getProfileSetInCurrentChannel = createSelector(
getCurrentChannelId,
getProfilesInChannel,