Manage member permissions (#223)
* Manage member permissions * Review feedback
This commit is contained in:
parent
088a2c7737
commit
8c0f5c70d7
12 changed files with 294 additions and 21 deletions
16
app/actions/views/channel_members.js
Normal file
16
app/actions/views/channel_members.js
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -102,7 +102,6 @@ export default class ChannelAddMembers extends PureComponent {
|
|||
loadingMembers={this.props.loadMoreRequestStatus === 'started'}
|
||||
selectable={true}
|
||||
onRowSelect={this.handleRowSelect}
|
||||
showSections={true}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
47
app/scenes/channel_members/channel_members_title.js
Normal file
47
app/scenes/channel_members/channel_members_title.js
Normal 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);
|
||||
57
app/scenes/channel_members/remove_member_button.js
Normal file
57
app/scenes/channel_members/remove_member_button.js
Normal 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);
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue