Merged contents of MoreDirectChannelsModal and ChannelMembersModal into UserList

Этот коммит содержится в:
hmhealey
2016-02-25 11:25:16 -05:00
коммит произвёл Harrison Healey
родитель af2a64b6bd
Коммит f7b04f0f27
6 изменённых файлов: 349 добавлений и 243 удалений

Просмотреть файл

@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx'; import LoadingScreen from './loading_screen.jsx';
import MemberList from './member_list.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx'; import ChannelInviteModal from './channel_invite_modal.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
@@ -24,6 +24,8 @@ export default class ChannelMembersModal extends React.Component {
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this); this.handleRemove = this.handleRemove.bind(this);
this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
// the rest of the state gets populated when the modal is shown // the rest of the state gets populated when the modal is shown
this.state = { this.state = {
showInviteModal: false showInviteModal: false
@@ -51,24 +53,10 @@ export default class ChannelMembersModal extends React.Component {
}; };
} }
const users = UserStore.getActiveOnlyProfiles(); // clone the member list since we mutate it later on
const memberList = extraInfo.members; const memberList = extraInfo.members.map((member) => {
return Object.assign({}, member);
const nonmemberList = []; });
for (const id in users) {
if (users.hasOwnProperty(id)) {
let found = false;
for (let i = 0; i < memberList.length; i++) {
if (memberList[i].id === id) {
found = true;
break;
}
}
if (!found) {
nonmemberList.push(users[id]);
}
}
}
function compareByUsername(a, b) { function compareByUsername(a, b) {
if (a.username < b.username) { if (a.username < b.username) {
@@ -81,15 +69,14 @@ export default class ChannelMembersModal extends React.Component {
} }
memberList.sort(compareByUsername); memberList.sort(compareByUsername);
nonmemberList.sort(compareByUsername);
return { return {
nonmemberList,
memberList, memberList,
loading: false loading: false
}; };
} }
onShow() { onShow() {
// TODO ugh
if ($(window).width() > 768) { if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
} }
@@ -116,41 +103,25 @@ export default class ChannelMembersModal extends React.Component {
this.setState(newState); this.setState(newState);
} }
} }
handleRemove(userId) { handleRemove(user) {
// Make sure the user is a member of the channel const userId = user.id;
const memberList = this.state.memberList;
let found = false;
for (let i = 0; i < memberList.length; i++) {
if (memberList[i].id === userId) {
found = true;
break;
}
}
if (!found) {
return;
}
const data = {}; const data = {};
data.user_id = userId; data.user_id = userId;
Client.removeChannelMember(ChannelStore.getCurrentId(), data, Client.removeChannelMember(
ChannelStore.getCurrentId(),
data,
() => { () => {
let oldMember; const memberList = this.state.memberList.slice();
for (let i = 0; i < memberList.length; i++) { for (let i = 0; i < memberList.length; i++) {
if (userId === memberList[i].id) { if (userId === memberList[i].id) {
oldMember = memberList[i];
memberList.splice(i, 1); memberList.splice(i, 1);
break; break;
} }
} }
const nonmemberList = this.state.nonmemberList; this.setState({memberList});
if (oldMember) {
nonmemberList.push(oldMember);
}
this.setState({memberList, nonmemberList});
AsyncClient.getChannelExtraInfo(); AsyncClient.getChannelExtraInfo();
}, },
(err) => { (err) => {
@@ -158,30 +129,39 @@ export default class ChannelMembersModal extends React.Component {
} }
); );
} }
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='member_item.removeMember'
defaultMessage='Remove Member'
/>
</button>
);
}
render() { render() {
var maxHeight = 1000; var maxHeight = 1000;
if (Utils.windowHeight() <= 1200) { if (Utils.windowHeight() <= 1200) {
maxHeight = Utils.windowHeight() - 300; maxHeight = Utils.windowHeight() - 300;
} }
const currentMember = ChannelStore.getCurrentMember();
let isAdmin = false;
if (currentMember) {
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
let content; let content;
if (this.state.loading) { if (this.state.loading) {
content = (<LoadingScreen/>); content = (<LoadingScreen/>);
} else { } else {
content = ( content = (
<div className='team-member-list'> <FilteredUserList
<MemberList users={this.state.memberList}
memberList={this.state.memberList} actions={[this.createRemoveMemberButton]}
isAdmin={isAdmin} />
handleRemove={this.handleRemove}
/>
</div>
); );
} }

129
web/react/components/filtered_user_list.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,129 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import UserList from './user_list.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
member: {
id: 'more_direct_channels.member',
defaultMessage: 'Member'
},
search: {
id: 'more_direct_channels.search',
defaultMessage: 'Search members'
}
});
class FilteredUserList extends React.Component {
constructor(props) {
super(props);
this.handleFilterChange = this.handleFilterChange.bind(this);
this.state = {
filter: ''
};
}
componentDidUpdate(prevProps, prevState) {
if (prevState.filter !== this.state.filter) {
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
}
}
handleFilterChange(e) {
this.setState({
filter: e.target.value
});
}
render() {
const {formatMessage} = this.props.intl;
let users = this.props.users;
if (this.state.filter) {
const filter = this.state.filter.toLowerCase();
users = users.filter((user) => {
return user.username.toLowerCase().indexOf(filter) !== -1 ||
(user.first_name && user.first_name.toLowerCase().indexOf(filter) !== -1) ||
(user.last_name && user.last_name.toLowerCase().indexOf(filter) !== -1) ||
(user.nickname && user.nickname.toLowerCase().indexOf(filter) !== -1);
});
}
let memberString = formatMessage(holders.member);
if (users.length !== 1) {
memberString += 's';
}
let count;
if (users.length === this.props.users.length) {
count = (
<FormattedMessage
id='more_direct_channels.count'
defaultMessage='{count} {member}'
values={{
count: users.length,
member: memberString
}}
/>
);
} else {
count = (
<FormattedMessage
id='more_direct_channels.countTotal'
defaultMessage='{count} {member} of {total} Total'
values={{
count: users.length,
member: memberString,
total: this.props.users.length
}}
/>
);
}
return (
<div>
<div className='filter-row'>
<div className='col-sm-6'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={formatMessage(holders.search)}
onInput={this.handleFilterChange}
/>
</div>
<div className='col-sm-6'>
<span className='member-count'>{count}</span>
</div>
</div>
<div
ref='userList'
className='user-list'
>
<UserList
users={users}
actions={this.props.actions}
/>
</div>
</div>
);
}
}
FilteredUserList.defaultProps = {
users: [],
actions: []
};
FilteredUserList.propTypes = {
intl: intlShape.isRequired,
users: React.PropTypes.arrayOf(React.PropTypes.object),
actions: React.PropTypes.arrayOf(React.PropTypes.func)
};
export default injectIntl(FilteredUserList);

Просмотреть файл

@@ -2,36 +2,24 @@
// See License.txt for license information. // See License.txt for license information.
const Modal = ReactBootstrap.Modal; const Modal = ReactBootstrap.Modal;
import FilteredUserList from './filtered_user_list.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx'; import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; import {FormattedMessage} from 'mm-intl';
const holders = defineMessages({ export default class MoreDirectChannels extends React.Component {
member: {
id: 'more_direct_channels.member',
defaultMessage: 'Member'
},
search: {
id: 'more_direct_channels.search',
defaultMessage: 'Search members'
}
});
class MoreDirectChannels extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleFilterChange = this.handleFilterChange.bind(this);
this.handleHide = this.handleHide.bind(this); this.handleHide = this.handleHide.bind(this);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.handleUserChange = this.handleUserChange.bind(this); this.handleUserChange = this.handleUserChange.bind(this);
this.createRowForUser = this.createRowForUser.bind(this); this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
this.state = { this.state = {
users: this.getUsersFromStore(), users: this.getUsersFromStore(),
filter: '',
loadingDMChannel: -1 loadingDMChannel: -1
}; };
} }
@@ -67,32 +55,20 @@ class MoreDirectChannels extends React.Component {
} }
onShow() { onShow() {
if (Utils.isMobile()) { // TODO ugh
$(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 250); /*if (Utils.isMobile()) {
$(ReactDOM.findDOMNode(this.refs.modal)).css('max-height', $(window).height() - 250);
} else { } else {
$(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); console.log(ReactDOM.findDOMNode(this.refs.modal));
$(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300); console.log($(ReactDOM.findDOMNode(this.refs.modal)));
} $(ReactDOM.findDOMNode(this.refs.modal)).css('max-height', $(window).height() - 300);
} }*/
handleFilterChange() {
const filter = ReactDOM.findDOMNode(this.refs.filter).value;
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
}
if (filter !== this.state.filter) {
this.setState({filter});
}
} }
handleHide() { handleHide() {
if (this.props.onModalDismissed) { if (this.props.onModalDismissed) {
this.props.onModalDismissed(); this.props.onModalDismissed();
} }
this.setState({filter: ''});
} }
handleShowDirectChannel(teammate, e) { handleShowDirectChannel(teammate, e) {
@@ -120,145 +96,34 @@ class MoreDirectChannels extends React.Component {
this.setState({users: this.getUsersFromStore()}); this.setState({users: this.getUsersFromStore()});
} }
createRowForUser(user) { createJoinDirectChannelButton({user}) {
const details = [];
const fullName = Utils.getFullName(user);
if (fullName) {
details.push(
<span
key={`${user.id}__full-name`}
className='full-name'
>
{fullName}
</span>
);
}
if (user.nickname) {
const separator = fullName ? ' - ' : '';
details.push(
<span
key={`${user.nickname}__nickname`}
>
{separator + user.nickname}
</span>
);
}
let joinButton;
if (this.state.loadingDMChannel === user.id) { if (this.state.loadingDMChannel === user.id) {
joinButton = ( return (
<img <img
className='channel-loading-gif' className='channel-loading-gif'
src='/static/images/load.gif' src='/static/images/load.gif'
/> />
); );
} else {
joinButton = (
<button
type='button'
className='btn btn-primary btn-message'
onClick={this.handleShowDirectChannel.bind(this, user)}
>
<FormattedMessage
id='more_direct_channels.message'
defaultMessage='Message'
/>
</button>
);
} }
return ( return (
<tr key={'direct-channel-row-user' + user.id}> <button
<td type='button'
key={user.id} className='btn btn-primary btn-message'
className='direct-channel' onClick={this.handleShowDirectChannel.bind(this, user)}
> >
<img <FormattedMessage
className='profile-img pull-left' id='more_direct_channels.message'
width='38' defaultMessage='Message'
height='38' />
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} </button>
/>
<div className='more-name'>
{user.username}
</div>
<div className='more-description'>
{details}
</div>
</td>
<td className='td--action lg'>
{joinButton}
</td>
</tr>
); );
} }
render() { render() {
const {formatMessage} = this.props.intl;
if (!this.props.show) {
return null;
}
let users = this.state.users;
if (this.state.filter) {
const filter = this.state.filter.toLowerCase();
users = users.filter((user) => {
return user.username.toLowerCase().indexOf(filter) !== -1 ||
user.first_name.toLowerCase().indexOf(filter) !== -1 ||
user.last_name.toLowerCase().indexOf(filter) !== -1 ||
user.nickname.toLowerCase().indexOf(filter) !== -1;
});
}
const userEntries = users.map(this.createRowForUser);
if (userEntries.length === 0) {
userEntries.push(
<tr key='no-users-found'><td>
<FormattedMessage
id='more_direct_channels.notFound'
defaultMessage='No users found :('
/>
</td></tr>);
}
let memberString = formatMessage(holders.member);
if (users.length !== 1) {
memberString += 's';
}
let count;
if (users.length === this.state.users.length) {
count = (
<FormattedMessage
id='more_direct_channels.count'
defaultMessage='{count} {member}'
values={{
count: users.length,
member: memberString
}}
/>
);
} else {
count = (
<FormattedMessage
id='more_direct_channels.countTotal'
defaultMessage='{count} {member} of {total} Total'
values={{
count: users.length,
member: memberString,
total: this.state.users.length
}}
/>
);
}
return ( return (
<Modal <Modal
dialogClassName='more-modal' dialogClassName='more-modal more-direct-channels'
show={this.props.show} show={this.props.show}
onHide={this.handleHide} onHide={this.handleHide}
> >
@@ -270,30 +135,11 @@ class MoreDirectChannels extends React.Component {
/> />
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body ref='modalBody'> <Modal.Body>
<div className='filter-row'> <FilteredUserList
<div className='col-sm-6'> users={this.state.users}
<input actions={[this.createJoinDirectChannelButton]}
ref='filter' />
className='form-control filter-textbox'
placeholder={formatMessage(holders.search)}
onInput={this.handleFilterChange}
/>
</div>
<div className='col-sm-6'>
<span className='member-count'>{count}</span>
</div>
</div>
<div
ref='userList'
className='user-list'
>
<table className='more-table table'>
<tbody>
{userEntries}
</tbody>
</table>
</div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<button <button
@@ -313,9 +159,6 @@ class MoreDirectChannels extends React.Component {
} }
MoreDirectChannels.propTypes = { MoreDirectChannels.propTypes = {
intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired, show: React.PropTypes.bool.isRequired,
onModalDismissed: React.PropTypes.func onModalDismissed: React.PropTypes.func
}; };
export default injectIntl(MoreDirectChannels);

53
web/react/components/user_list.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,53 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
import UserListRow from './user_list_row.jsx';
export default class UserList extends React.Component {
render() {
const users = this.props.users;
let content;
if (users.length > 0) {
content = users.map((user) => {
return (
<UserListRow
key={user.id}
user={user}
actions={this.props.actions}
/>
);
});
} else {
content = (
<tr key='no-users-found'>
<td>
<FormattedMessage
id='more_direct_channels.notFound'
defaultMessage='No users found :('
/>
</td>
</tr>
);
}
return (
<table className='more-table table'>
<tbody>
{content}
</tbody>
</table>
);
}
}
UserList.defaultProps = {
users: [],
actions: []
};
UserList.propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
actions: React.PropTypes.arrayOf(React.PropTypes.func)
};

79
web/react/components/user_list_row.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,79 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
export default function UserListRow({user, actions}) {
const details = [];
const fullName = Utils.getFullName(user);
if (fullName) {
details.push(
<span
key={`${user.id}__full-name`}
className='full-name'
>
{fullName}
</span>
);
}
if (user.nickname) {
const separator = fullName ? ' - ' : '';
details.push(
<span
key={`${user.nickname}__nickname`}
>
{separator + user.nickname}
</span>
);
}
const buttons = actions.map((Action, index) => {
return (
<Action
key={index.toString()}
user={user}
/>
);
});
return (
<tr>
<td
key={user.id}
className='direct-channel'
style={{display: 'flex'}}
>
<img
className='profile-img'
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
/>
<div
className='user-list-item__details'
>
<div className='more-name'>
{user.username}
</div>
<div className='more-description'>
{details}
</div>
</div>
<div
className='user-list-item__actions'
>
{buttons}
</div>
</td>
</tr>
);
}
UserListRow.defaultProps = {
actions: []
};
UserListRow.propTypes = {
user: React.PropTypes.object.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func)
};

Просмотреть файл

@@ -434,3 +434,25 @@
max-height: 150px; max-height: 150px;
} }
} }
.user-list {
display: flex;
flex-direction: column;
.profile-img {
width: 38px;
height: 38px;
flex-grow: 0;
flex-shrink: 0;
}
.user-list-item__details {
flex-grow: 1;
flex-shrink: 1;
}
.user-list-item__actions {
flex-grow: 0;
flex-shrink: 0;
}
}