PLT-5049 (Webapp) New Channel Members UI. (#5036)
This replaces the existing Channel Members UI with one based on the Team Members UI, so that either a button, a role or a role with a menu can be displayed. Basic logic for which actions and roles are displayed is implemented, although this doesn't change behaviour or functionality at all, as that will come in later PRs. It does, however, add code to fetch the ChannelMember objects as that is necessary to provide the full set of actions and roles as intended.
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
2010f74a21
Коммит
95251258f5
180
webapp/components/channel_members_dropdown.jsx
Обычный файл
180
webapp/components/channel_members_dropdown.jsx
Обычный файл
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import {removeUserFromChannel} from 'actions/channel_actions.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
export default class ChannelMembersDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleRemoveFromChannel = this.handleRemoveFromChannel.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: null,
|
||||
user: null,
|
||||
role: null
|
||||
};
|
||||
}
|
||||
|
||||
handleRemoveFromChannel() {
|
||||
removeUserFromChannel(
|
||||
this.props.channel.id,
|
||||
this.props.user.id,
|
||||
() => {
|
||||
AsyncClient.getChannelStats(this.props.channel.id);
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Checks if the user this menu is for is a channel admin or not.
|
||||
isChannelAdmin() {
|
||||
if (Utils.isChannelAdmin(this.props.channelMember.roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if the current user has the power to change the roles of this member.
|
||||
canChangeMemberRoles() {
|
||||
if (UserStore.isSystemAdminForCurrentUser()) {
|
||||
return true;
|
||||
} else if (TeamStore.isTeamAdminForCurrentTeam()) {
|
||||
return true;
|
||||
} else if (ChannelStore.isChannelAdminForCurrentChannel()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if the current user has the power to remove this member from the channel.
|
||||
canRemoveMember() {
|
||||
// TODO: This will be implemented as part of PLT-5047.
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = (
|
||||
<div className='has-error'>
|
||||
<label className='has-error control-label'>{this.state.serverError}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.user.id === UserStore.getCurrentId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.canChangeMemberRoles()) {
|
||||
let role = (
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.channel_member'
|
||||
defaultMessage='Channel Member'
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.isChannelAdmin()) {
|
||||
role = (
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.channel_admin'
|
||||
defaultMessage='Channel Admin'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let removeFromChannel = null;
|
||||
if (this.canRemoveMember()) {
|
||||
removeFromChannel = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleRemoveFromChannel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.remove_from_channel'
|
||||
defaultMessage='Remove From Channel'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='dropdown member-drop'>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle theme'
|
||||
type='button'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='true'
|
||||
>
|
||||
<span>{role} </span>
|
||||
<span className='fa fa-chevron-down'/>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu member-menu'
|
||||
role='menu'
|
||||
>
|
||||
{removeFromChannel}
|
||||
</ul>
|
||||
{serverError}
|
||||
</div>
|
||||
);
|
||||
} else if (this.canRemoveMember()) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-danger btn-message'
|
||||
onClick={this.handleRemoveFromChannel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.remove_member'
|
||||
defaultMessage='Remove Member'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
} else if (this.isChannelAdmin()) {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.channel_admin'
|
||||
defaultMessage='Channel Admin'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='channel_members_dropdown.channel_member'
|
||||
defaultMessage='Channel Member'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelMembersDropdown.propTypes = {
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
user: React.PropTypes.object.isRequired,
|
||||
teamMember: React.PropTypes.object.isRequired,
|
||||
channelMember: React.PropTypes.object.isRequired
|
||||
};
|
||||
@@ -1,170 +1,29 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SearchableUserList from './searchable_user_list.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {searchUsers} from 'actions/user_actions.jsx';
|
||||
import {removeUserFromChannel} from 'actions/channel_actions.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import MemberListChannel from './member_list_channel.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const USERS_PER_PAGE = 50;
|
||||
|
||||
export default class ChannelMembersModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onStatusChange = this.onStatusChange.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
|
||||
this.term = '';
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
const stats = ChannelStore.getStats(props.channel.id);
|
||||
|
||||
this.state = {
|
||||
users: [],
|
||||
total: stats.member_count,
|
||||
show: true,
|
||||
search: false,
|
||||
statusChange: false
|
||||
channel: this.props.channel,
|
||||
show: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addStatsChangeListener(this.onChange);
|
||||
UserStore.addInChannelChangeListener(this.onChange);
|
||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
||||
|
||||
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeStatsChangeListener(this.onChange);
|
||||
UserStore.removeInChannelChangeListener(this.onChange);
|
||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
||||
}
|
||||
|
||||
onChange(force) {
|
||||
if (this.state.search && !force) {
|
||||
this.search(this.term);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = ChannelStore.getStats(this.props.channel.id);
|
||||
this.setState({
|
||||
users: UserStore.getProfileListInChannel(this.props.channel.id),
|
||||
total: stats.member_count
|
||||
});
|
||||
}
|
||||
|
||||
onStatusChange() {
|
||||
// Initiate a render to pick up on new statuses
|
||||
this.setState({
|
||||
statusChange: !this.state.statusChange
|
||||
});
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.setState({show: false});
|
||||
}
|
||||
|
||||
handleRemove(user) {
|
||||
const userId = user.id;
|
||||
|
||||
removeUserFromChannel(
|
||||
this.props.channel.id,
|
||||
userId,
|
||||
null,
|
||||
(err) => {
|
||||
this.setState({inviteError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createRemoveMemberButton({user}) {
|
||||
if (user.id === UserStore.getCurrentId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary btn-message'
|
||||
onClick={this.handleRemove.bind(this, user)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_modal.remove'
|
||||
defaultMessage='Remove'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
nextPage(page) {
|
||||
AsyncClient.getProfilesInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
|
||||
}
|
||||
|
||||
search(term) {
|
||||
this.term = term;
|
||||
|
||||
if (term === '') {
|
||||
this.onChange(true);
|
||||
this.setState({search: false});
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(
|
||||
() => {
|
||||
searchUsers(
|
||||
term,
|
||||
TeamStore.getCurrentId(),
|
||||
{in_channel_id: this.props.channel.id},
|
||||
(users) => {
|
||||
this.setState({search: true, users});
|
||||
}
|
||||
);
|
||||
},
|
||||
Constants.SEARCH_TIMEOUT_MILLISECONDS
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.loading) {
|
||||
content = (<LoadingScreen/>);
|
||||
} else {
|
||||
content = (
|
||||
<SearchableUserList
|
||||
users={this.state.users}
|
||||
usersPerPage={USERS_PER_PAGE}
|
||||
total={this.state.total}
|
||||
nextPage={this.nextPage}
|
||||
search={this.search}
|
||||
actions={[this.createRemoveMemberButton]}
|
||||
focusOnMount={!UserAgent.isMobile()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
@@ -177,7 +36,7 @@ export default class ChannelMembersModal extends React.Component {
|
||||
<Modal.Title>
|
||||
<span className='name'>{this.props.channel.display_name}</span>
|
||||
<FormattedMessage
|
||||
id='channel_memebers_modal.members'
|
||||
id='channel_members_modal.members'
|
||||
defaultMessage=' Members'
|
||||
/>
|
||||
</Modal.Title>
|
||||
@@ -198,7 +57,9 @@ export default class ChannelMembersModal extends React.Component {
|
||||
<Modal.Body
|
||||
ref='modalBody'
|
||||
>
|
||||
{content}
|
||||
<MemberListChannel
|
||||
channel={this.props.channel}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
173
webapp/components/member_list_channel.jsx
Обычный файл
173
webapp/components/member_list_channel.jsx
Обычный файл
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx';
|
||||
import SearchableUserList from 'components/searchable_user_list.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {searchUsers, loadProfilesAndTeamMembersAndChannelMembers, loadTeamMembersAndChannelMembersForProfilesList} from 'actions/user_actions.jsx';
|
||||
import {getChannelStats} from 'utils/async_client.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const USERS_PER_PAGE = 50;
|
||||
|
||||
export default class MemberListChannel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onStatsChange = this.onStatsChange.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.loadComplete = this.loadComplete.bind(this);
|
||||
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
const stats = ChannelStore.getCurrentStats();
|
||||
|
||||
this.state = {
|
||||
users: UserStore.getProfileListInChannel(),
|
||||
teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
|
||||
channelMembers: Object.assign({}, ChannelStore.getMembersInChannel()),
|
||||
total: stats.member_count,
|
||||
search: false,
|
||||
term: '',
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.addInTeamChangeListener(this.onChange);
|
||||
UserStore.addStatusesChangeListener(this.onChange);
|
||||
TeamStore.addChangeListener(this.onChange);
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
ChannelStore.addStatsChangeListener(this.onStatsChange);
|
||||
|
||||
loadProfilesAndTeamMembersAndChannelMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
|
||||
getChannelStats(ChannelStore.getCurrentId());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.removeInTeamChangeListener(this.onChange);
|
||||
UserStore.removeStatusesChangeListener(this.onChange);
|
||||
TeamStore.removeChangeListener(this.onChange);
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
ChannelStore.removeStatsChangeListener(this.onStatsChange);
|
||||
}
|
||||
|
||||
loadComplete() {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
onChange(force) {
|
||||
if (this.state.search && !force) {
|
||||
return;
|
||||
} else if (this.state.search) {
|
||||
this.search(this.state.term);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
users: UserStore.getProfileListInChannel(),
|
||||
teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
|
||||
channelMembers: Object.assign({}, ChannelStore.getMembersInChannel())
|
||||
});
|
||||
}
|
||||
|
||||
onStatsChange() {
|
||||
const stats = ChannelStore.getCurrentStats();
|
||||
this.setState({total: stats.member_count});
|
||||
}
|
||||
|
||||
nextPage(page) {
|
||||
loadProfilesAndTeamMembersAndChannelMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
|
||||
}
|
||||
|
||||
search(term) {
|
||||
if (term === '') {
|
||||
this.setState({
|
||||
search: false,
|
||||
term,
|
||||
users: UserStore.getProfileListInChannel(),
|
||||
teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
|
||||
channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(
|
||||
() => {
|
||||
searchUsers(
|
||||
term,
|
||||
TeamStore.getCurrentId(),
|
||||
{},
|
||||
(users) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
search: true,
|
||||
users,
|
||||
term,
|
||||
teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
|
||||
channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
|
||||
});
|
||||
loadTeamMembersAndChannelMembersForProfilesList(users, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
|
||||
}
|
||||
);
|
||||
},
|
||||
Constants.SEARCH_TIMEOUT_MILLISECONDS
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const teamMembers = this.state.teamMembers;
|
||||
const channelMembers = this.state.channelMembers;
|
||||
const users = this.state.users;
|
||||
const actionUserProps = {};
|
||||
|
||||
let usersToDisplay;
|
||||
if (this.state.loading) {
|
||||
usersToDisplay = null;
|
||||
} else {
|
||||
usersToDisplay = [];
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
|
||||
if (teamMembers[user.id] && channelMembers[user.id]) {
|
||||
usersToDisplay.push(user);
|
||||
actionUserProps[user.id] = {
|
||||
channel: this.props.channel,
|
||||
teamMember: teamMembers[user.id],
|
||||
channelMember: channelMembers[user.id]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchableUserList
|
||||
users={usersToDisplay}
|
||||
usersPerPage={USERS_PER_PAGE}
|
||||
total={this.state.total}
|
||||
nextPage={this.nextPage}
|
||||
search={this.search}
|
||||
actions={[ChannelMembersDropdown]}
|
||||
actionUserProps={actionUserProps}
|
||||
focusOnMount={!UserAgent.isMobile()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MemberListChannel.propTypes = {
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
Ссылка в новой задаче
Block a user