diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/channel_members_modal.jsx | 94 | ||||
-rw-r--r-- | web/react/components/filtered_user_list.jsx | 129 | ||||
-rw-r--r-- | web/react/components/more_direct_channels.jsx | 215 | ||||
-rw-r--r-- | web/react/components/user_list.jsx | 53 | ||||
-rw-r--r-- | web/react/components/user_list_row.jsx | 79 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_modal.scss | 22 |
6 files changed, 349 insertions, 243 deletions
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx index fd452f206..44de0266b 100644 --- a/web/react/components/channel_members_modal.jsx +++ b/web/react/components/channel_members_modal.jsx @@ -1,8 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import FilteredUserList from './filtered_user_list.jsx'; import LoadingScreen from './loading_screen.jsx'; -import MemberList from './member_list.jsx'; import ChannelInviteModal from './channel_invite_modal.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.handleRemove = this.handleRemove.bind(this); + this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this); + // the rest of the state gets populated when the modal is shown this.state = { showInviteModal: false @@ -51,24 +53,10 @@ export default class ChannelMembersModal extends React.Component { }; } - const users = UserStore.getActiveOnlyProfiles(); - const memberList = extraInfo.members; - - 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]); - } - } - } + // clone the member list since we mutate it later on + const memberList = extraInfo.members.map((member) => { + return Object.assign({}, member); + }); function compareByUsername(a, b) { if (a.username < b.username) { @@ -81,15 +69,14 @@ export default class ChannelMembersModal extends React.Component { } memberList.sort(compareByUsername); - nonmemberList.sort(compareByUsername); return { - nonmemberList, memberList, loading: false }; } onShow() { + // TODO ugh if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } @@ -116,41 +103,25 @@ export default class ChannelMembersModal extends React.Component { this.setState(newState); } } - handleRemove(userId) { - // Make sure the user is a member of the channel - 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; - } + handleRemove(user) { + const userId = user.id; const data = {}; 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++) { if (userId === memberList[i].id) { - oldMember = memberList[i]; memberList.splice(i, 1); break; } } - const nonmemberList = this.state.nonmemberList; - if (oldMember) { - nonmemberList.push(oldMember); - } - - this.setState({memberList, nonmemberList}); + this.setState({memberList}); AsyncClient.getChannelExtraInfo(); }, (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() { var maxHeight = 1000; if (Utils.windowHeight() <= 1200) { 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; if (this.state.loading) { content = (<LoadingScreen/>); } else { content = ( - <div className='team-member-list'> - <MemberList - memberList={this.state.memberList} - isAdmin={isAdmin} - handleRemove={this.handleRemove} - /> - </div> + <FilteredUserList + users={this.state.memberList} + actions={[this.createRemoveMemberButton]} + /> ); } diff --git a/web/react/components/filtered_user_list.jsx b/web/react/components/filtered_user_list.jsx new file mode 100644 index 000000000..dc8abd4c1 --- /dev/null +++ b/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); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 3b72b251c..c85b5e9c5 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -2,36 +2,24 @@ // See License.txt for license information. const Modal = ReactBootstrap.Modal; +import FilteredUserList from './filtered_user_list.jsx'; import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {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 MoreDirectChannels extends React.Component { +export default class MoreDirectChannels extends React.Component { constructor(props) { super(props); - this.handleFilterChange = this.handleFilterChange.bind(this); this.handleHide = this.handleHide.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); this.handleUserChange = this.handleUserChange.bind(this); - this.createRowForUser = this.createRowForUser.bind(this); + this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this); this.state = { users: this.getUsersFromStore(), - filter: '', loadingDMChannel: -1 }; } @@ -67,32 +55,20 @@ class MoreDirectChannels extends React.Component { } onShow() { - if (Utils.isMobile()) { - $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 250); + // TODO ugh + /*if (Utils.isMobile()) { + $(ReactDOM.findDOMNode(this.refs.modal)).css('max-height', $(window).height() - 250); } else { - $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); - $(ReactDOM.findDOMNode(this.refs.userList)).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}); - } + console.log(ReactDOM.findDOMNode(this.refs.modal)); + console.log($(ReactDOM.findDOMNode(this.refs.modal))); + $(ReactDOM.findDOMNode(this.refs.modal)).css('max-height', $(window).height() - 300); + }*/ } handleHide() { if (this.props.onModalDismissed) { this.props.onModalDismissed(); } - - this.setState({filter: ''}); } handleShowDirectChannel(teammate, e) { @@ -120,145 +96,34 @@ class MoreDirectChannels extends React.Component { this.setState({users: this.getUsersFromStore()}); } - createRowForUser(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; + createJoinDirectChannelButton({user}) { if (this.state.loadingDMChannel === user.id) { - joinButton = ( + return ( <img className='channel-loading-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 ( - <tr key={'direct-channel-row-user' + user.id}> - <td - key={user.id} - className='direct-channel' - > - <img - className='profile-img pull-left' - width='38' - height='38' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} - /> - <div className='more-name'> - {user.username} - </div> - <div className='more-description'> - {details} - </div> - </td> - <td className='td--action lg'> - {joinButton} - </td> - </tr> + <button + type='button' + className='btn btn-primary btn-message' + onClick={this.handleShowDirectChannel.bind(this, user)} + > + <FormattedMessage + id='more_direct_channels.message' + defaultMessage='Message' + /> + </button> ); } 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 ( <Modal - dialogClassName='more-modal' + dialogClassName='more-modal more-direct-channels' show={this.props.show} onHide={this.handleHide} > @@ -270,30 +135,11 @@ class MoreDirectChannels extends React.Component { /> </Modal.Title> </Modal.Header> - <Modal.Body ref='modalBody'> - <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' - > - <table className='more-table table'> - <tbody> - {userEntries} - </tbody> - </table> - </div> + <Modal.Body> + <FilteredUserList + users={this.state.users} + actions={[this.createJoinDirectChannelButton]} + /> </Modal.Body> <Modal.Footer> <button @@ -313,9 +159,6 @@ class MoreDirectChannels extends React.Component { } MoreDirectChannels.propTypes = { - intl: intlShape.isRequired, show: React.PropTypes.bool.isRequired, onModalDismissed: React.PropTypes.func }; - -export default injectIntl(MoreDirectChannels); diff --git a/web/react/components/user_list.jsx b/web/react/components/user_list.jsx new file mode 100644 index 000000000..e3c2ad08d --- /dev/null +++ b/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) +}; diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx new file mode 100644 index 000000000..cf2e2668c --- /dev/null +++ b/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) +}; diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 09c366c77..6144dc611 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -434,3 +434,25 @@ 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; + } +} |