diff options
Diffstat (limited to 'webapp')
-rw-r--r-- | webapp/actions/team_actions.jsx | 25 | ||||
-rw-r--r-- | webapp/actions/user_actions.jsx | 22 | ||||
-rw-r--r-- | webapp/actions/websocket_actions.jsx | 25 | ||||
-rw-r--r-- | webapp/client/client.jsx | 69 | ||||
-rw-r--r-- | webapp/components/add_users_to_team.jsx | 270 | ||||
-rw-r--r-- | webapp/components/sidebar_header_dropdown.jsx | 185 | ||||
-rw-r--r-- | webapp/components/sidebar_right_menu.jsx | 70 | ||||
-rwxr-xr-x | webapp/i18n/en.json | 7 | ||||
-rw-r--r-- | webapp/stores/team_store.jsx | 10 | ||||
-rw-r--r-- | webapp/stores/user_store.jsx | 107 | ||||
-rw-r--r-- | webapp/utils/async_client.jsx | 58 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 3 |
12 files changed, 770 insertions, 81 deletions
diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 4cb57961b..b091692f8 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -114,6 +114,31 @@ export function addUserToTeamFromInvite(data, hash, inviteId, success, error) { ); } +export function addUsersToTeam(teamId, userIds, success, error) { + Client.addUsersToTeam( + teamId, + userIds, + (teamMembers) => { + teamMembers.forEach((member) => { + TeamStore.removeMemberNotInTeam(teamId, member.user_id); + UserStore.removeProfileNotInTeam(teamId, member.user_id); + }); + UserStore.emitNotInTeamChange(); + + if (success) { + success(teamMembers); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'addUsersToTeam'); + + if (error) { + error(err); + } + } + ); +} + export function getInviteInfo(inviteId, success, error) { Client.getInviteInfo( inviteId, diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index b9d4ec376..1ab85922d 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -492,6 +492,28 @@ export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = { ); } +export function searchUsersNotInTeam(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) { + Client.searchUsersNotInTeam( + term, + teamId, + options, + (data) => { + loadStatusesForProfilesList(data); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'searchUsersNotInTeam'); + + if (error) { + error(err); + } + } + ); +} + export function autocompleteUsersInChannel(username, channelId, success, error) { Client.autocompleteUsersInChannel( username, diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index ab798df28..e9ebea472 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -153,6 +153,10 @@ function handleEvent(msg) { handleUpdateTeamEvent(msg); break; + case SocketEvents.ADDED_TO_TEAM: + handleTeamAddedEvent(msg); + break; + case SocketEvents.USER_ADDED: handleUserAddedEvent(msg); break; @@ -241,6 +245,27 @@ function handlePostDeleteEvent(msg) { GlobalActions.emitPostDeletedEvent(post); } +function handleTeamAddedEvent(msg) { + Client.getTeam(msg.data.team_id, (team) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_TEAM, + team + }); + + Client.getMyTeamMembers((data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS, + team_members: data + }); + AsyncClient.getMyTeamsUnread(); + }, (err) => { + AsyncClient.dispatchError(err, 'getMyTeamMembers'); + }); + }, (err) => { + AsyncClient.dispatchError(err, 'getTeam'); + }); +} + function handleLeaveTeamEvent(msg) { if (UserStore.getCurrentId() === msg.data.user_id) { TeamStore.removeMyTeamMember(msg.data.team_id); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 1f70300e8..c1a9d2f85 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -489,6 +489,15 @@ export default class Client { // Team Routes Section + getTeam(teamId, success, error) { + request. + get(`${this.getTeamsRoute()}/${teamId}/me`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeam', success, error)); + } + findTeamByName(teamName, success, error) { request. post(`${this.getTeamsRoute()}/find_team_by_name`). @@ -681,6 +690,30 @@ export default class Client { this.trackEvent('api', 'api_teams_invite_members'); } + addUsersToTeam(teamId, userIds, success, error) { + let nonEmptyTeamId = teamId; + if (nonEmptyTeamId === '') { + nonEmptyTeamId = this.getTeamId(); + } + + const teamMembers = userIds.map((userId) => { + return { + team_id: nonEmptyTeamId, + user_id: userId + }; + }); + + request. + post(`${this.url}/api/v4/teams/${nonEmptyTeamId}/members/batch`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(teamMembers). + end(this.handleResponse.bind(this, 'addUsersToTeam', success, error)); + + this.trackEvent('api', 'api_teams_batch_add_members', {team_id: nonEmptyTeamId, count: teamMembers.length}); + } + removeUserFromTeam(teamId, userId, success, error) { let nonEmptyTeamId = teamId; if (nonEmptyTeamId === '') { @@ -1124,6 +1157,29 @@ export default class Client { this.trackEvent('api', 'api_profiles_get_in_team', {team_id: teamId}); } + getProfilesNotInTeam(teamId, offset, limit, success, error) { + // Super hacky, but this option only exists in api v4 + function wrappedSuccess(data, res) { + // Convert the profile list provided by api v4 to a map to match similar v3 calls + const profiles = {}; + + for (const profile of data) { + profiles[profile.id] = profile; + } + + success(profiles, res); + } + + request. + get(`${this.url}/api/v4/users?not_in_team=${this.getTeamId()}&page=${offset}&per_page=${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfilesNotInTeam', wrappedSuccess, error)); + + this.trackEvent('api', 'api_profiles_get_not_in_team', {team_id: teamId}); + } + getProfilesInChannel(channelId, offset, limit, success, error) { request. get(`${this.getChannelNeededRoute(channelId)}/users/${offset}/${limit}`). @@ -1191,6 +1247,19 @@ export default class Client { end(this.handleResponse.bind(this, 'searchUsers', success, error)); } + searchUsersNotInTeam(term, teamId, options, success, error) { + // Note that this is calling an APIv4 Endpoint since no APIv3 equivalent exists. + request. + post(`${this.url}/api/v4/users/search`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({term, not_in_team_id: teamId, ...options}). + end(this.handleResponse.bind(this, 'searchUsersNotInTeam', success, error)); + + this.trackEvent('api', 'api_search_users_not_in_team', {team_id: teamId}); + } + autocompleteUsersInChannel(term, channelId, success, error) { request. get(`${this.getChannelNeededRoute(channelId)}/users/autocomplete?term=${encodeURIComponent(term)}`). diff --git a/webapp/components/add_users_to_team.jsx b/webapp/components/add_users_to_team.jsx new file mode 100644 index 000000000..fce651d9f --- /dev/null +++ b/webapp/components/add_users_to_team.jsx @@ -0,0 +1,270 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import MultiSelect from 'components/multiselect/multiselect.jsx'; +import ProfilePicture from 'components/profile_picture.jsx'; + +import {addUsersToTeam} from 'actions/team_actions.jsx'; +import {searchUsersNotInTeam} from 'actions/user_actions.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import Constants from 'utils/constants.jsx'; +import {displayUsernameForUser} from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +const USERS_PER_PAGE = 50; +const MAX_SELECTABLE_VALUES = 20; + +export default class AddUsersToTeam extends React.Component { + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleExit = this.handleExit.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.onChange = this.onChange.bind(this); + this.search = this.search.bind(this); + this.addValue = this.addValue.bind(this); + + this.searchTimeoutId = 0; + + this.state = { + users: null, + values: [], + show: true, + search: false + }; + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); + UserStore.addNotInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + + AsyncClient.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + UserStore.removeNotInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + } + + handleHide() { + this.setState({show: false}); + } + + handleExit() { + if (this.exitToChannel) { + browserHistory.push(this.exitToChannel); + } + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSubmit(e) { + if (e) { + e.preventDefault(); + } + + const userIds = this.state.values.map((v) => v.id); + if (userIds.length === 0) { + return; + } + + addUsersToTeam(TeamStore.getCurrentId(), userIds); + + this.handleHide(); + } + + addValue(value) { + const values = Object.assign([], this.state.values); + if (values.indexOf(value) === -1) { + values.push(value); + } + + this.setState({values}); + } + + onChange(force) { + if (this.state.search && !force) { + return; + } + + const users = Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)); + + for (let i = 0; i < users.length; i++) { + const user = Object.assign({}, users[i]); + user.value = user.id; + user.label = '@' + user.username; + users[i] = user; + } + + this.setState({ + users + }); + } + + handlePageChange(page, prevPage) { + if (page > prevPage) { + AsyncClient.getProfilesNotInTeam((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + } + + search(term) { + clearTimeout(this.searchTimeoutId); + + if (term === '') { + this.onChange(true); + this.setState({search: false}); + this.searchTimeoutId = ''; + return; + } + + const teamId = TeamStore.getCurrentId(); + + const searchTimeoutId = setTimeout( + () => { + searchUsersNotInTeam( + term, + teamId, + {}, + (users) => { + if (searchTimeoutId !== this.searchTimeoutId) { + return; + } + + let indexToDelete = -1; + for (let i = 0; i < users.length; i++) { + if (users[i].id === UserStore.getCurrentId()) { + indexToDelete = i; + } + users[i].value = users[i].id; + users[i].label = '@' + users[i].username; + } + + if (indexToDelete !== -1) { + users.splice(indexToDelete, 1); + } + this.setState({search: true, users}); + } + ); + }, + Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + + this.searchTimeoutId = searchTimeoutId; + } + + handleDelete(values) { + this.setState({values}); + } + + renderOption(option, isSelected, onAdd) { + var rowSelected = ''; + if (isSelected) { + rowSelected = 'more-modal__row--selected'; + } + + return ( + <div + key={option.id} + ref={isSelected ? 'selected' : option.id} + className={'more-modal__row clickable ' + rowSelected} + onClick={() => onAdd(option)} + > + <ProfilePicture + src={`${Client.getUsersRoute()}/${option.id}/image?time=${option.last_picture_update}`} + width='32' + height='32' + /> + <div + className='more-modal__details' + > + <div className='more-modal__name'> + {displayUsernameForUser(option)} + </div> + <div className='more-modal__description'> + {option.email} + </div> + </div> + <div className='more-modal__actions'> + <div className='more-modal__actions--round'> + <i className='fa fa-plus'/> + </div> + </div> + </div> + ); + } + + renderValue(user) { + return user.username; + } + + render() { + const numRemainingText = ( + <FormattedMessage + id='multiselect.numPeopleRemaining' + defaultMessage='You can add {num, number} more {num, plural, =0 {people} one {person} other {people}}. ' + values={{ + num: MAX_SELECTABLE_VALUES - this.state.values.length + }} + /> + ); + + return ( + <Modal + dialogClassName={'more-modal more-direct-channels'} + show={this.state.show} + onHide={this.handleHide} + onExited={this.handleExit} + > + <Modal.Header closeButton={true}> + <Modal.Title> + <FormattedMessage + id='add_users_to_team.title' + defaultMessage='Add New Members To {teamName} Team' + values={{ + teamName: ( + <strong>{TeamStore.getCurrent().display_name}</strong> + ) + }} + /> + </Modal.Title> + </Modal.Header> + <Modal.Body> + <MultiSelect + key='addUsersToTeamKey' + options={this.state.users} + optionRenderer={this.renderOption} + values={this.state.values} + valueRenderer={this.renderValue} + perPage={USERS_PER_PAGE} + handlePageChange={this.handlePageChange} + handleInput={this.search} + handleDelete={this.handleDelete} + handleAdd={this.addValue} + handleSubmit={this.handleSubmit} + maxValues={MAX_SELECTABLE_VALUES} + numRemainingText={numRemainingText} + /> + </Modal.Body> + </Modal> + ); + } +} + +AddUsersToTeam.propTypes = { + onModalDismissed: React.PropTypes.func +}; diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index a0a0205be..baa8aae7b 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -14,6 +14,7 @@ import AboutBuildModal from './about_build_modal.jsx'; import SidebarHeaderDropdownButton from './sidebar_header_dropdown_button.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; +import AddUsersToTeam from './add_users_to_team.jsx'; import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; @@ -43,6 +44,8 @@ export default class SidebarHeaderDropdown extends React.Component { this.handleAboutModal = this.handleAboutModal.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.toggleAccountSettingsModal = this.toggleAccountSettingsModal.bind(this); + this.showAddUsersToTeamModal = this.showAddUsersToTeamModal.bind(this); + this.hideAddUsersToTeamModal = this.hideAddUsersToTeamModal.bind(this); this.showInviteMemberModal = this.showInviteMemberModal.bind(this); this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this); this.showTeamMembersModal = this.showTeamMembersModal.bind(this); @@ -61,7 +64,8 @@ export default class SidebarHeaderDropdown extends React.Component { showAboutModal: false, showDropdown: false, showTeamMembersModal: false, - showUserSettingsModal: false + showUserSettingsModal: false, + showAddUsersToTeamModal: false }; } @@ -102,6 +106,21 @@ export default class SidebarHeaderDropdown extends React.Component { }); } + showAddUsersToTeamModal(e) { + e.preventDefault(); + + this.setState({ + showAddUsersToTeamModal: true, + showDropdown: false + }); + } + + hideAddUsersToTeamModal() { + this.setState({ + showAddUsersToTeamModal: false + }); + } + showInviteMemberModal(e) { e.preventDefault(); @@ -181,6 +200,7 @@ export default class SidebarHeaderDropdown extends React.Component { const currentUser = this.props.currentUser; let teamLink = ''; let inviteLink = ''; + let addMemberToTeam = ''; let manageLink = ''; let sysAdminLink = ''; let isAdmin = false; @@ -204,7 +224,22 @@ export default class SidebarHeaderDropdown extends React.Component { > <FormattedMessage id='navbar_dropdown.inviteMember' - defaultMessage='Invite New Member' + defaultMessage='Send Email Invite' + /> + </a> + </li> + ); + + addMemberToTeam = ( + <li> + <a + id='addUsersToTeam' + href='#' + onClick={this.showAddUsersToTeamModal} + > + <FormattedMessage + id='navbar_dropdown.addMemberToTeam' + defaultMessage='Add Members to Team' /> </a> </li> @@ -230,9 +265,11 @@ export default class SidebarHeaderDropdown extends React.Component { if (config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; + addMemberToTeam = null; } else if (config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; + addMemberToTeam = null; } } } @@ -416,10 +453,8 @@ export default class SidebarHeaderDropdown extends React.Component { ); } - let nativeAppDivider = null; let nativeAppLink = null; if (global.window.mm_config.AppDownloadLink && !UserAgent.isMobileApp()) { - nativeAppDivider = <li className='divider'/>; nativeAppLink = ( <li> <Link @@ -447,6 +482,92 @@ export default class SidebarHeaderDropdown extends React.Component { ); } + let addUsersToTeamModal; + if (this.state.showAddUsersToTeamModal) { + addUsersToTeamModal = ( + <AddUsersToTeam + onModalDismissed={this.hideAddUsersToTeamModal} + /> + ); + } + + const accountSettings = ( + <li> + <a + id='accountSettings' + href='#' + onClick={this.toggleAccountSettingsModal} + > + <FormattedMessage + id='navbar_dropdown.accountSettings' + defaultMessage='Account Settings' + /> + </a> + </li> + ); + + const about = ( + <li> + <a + href='#' + onClick={this.handleAboutModal} + > + <FormattedMessage + id='navbar_dropdown.about' + defaultMessage='About Mattermost' + /> + </a> + </li> + ); + + const logout = ( + <li> + <a + id='logout' + href='#' + onClick={() => GlobalActions.emitUserLoggedOutEvent()} + > + <FormattedMessage + id='navbar_dropdown.logout' + defaultMessage='Logout' + /> + </a> + </li> + ); + + const customEmoji = this.renderCustomEmojiLink(); + + // Dividers. + let inviteDivider = null; + if (inviteLink || teamLink || addMemberToTeam) { + inviteDivider = <li className='divider'/>; + } + + let teamDivider = null; + if (teamSettings || manageLink || teams) { + teamDivider = <li className='divider'/>; + } + + let backstageDivider = null; + if (integrationsLink || customEmoji) { + backstageDivider = <li className='divider'/>; + } + + let sysAdminDivider = null; + if (sysAdminLink) { + sysAdminDivider = <li className='divider'/>; + } + + let helpDivider = null; + if (helpLink || reportLink || nativeAppLink || about) { + helpDivider = <li className='divider'/>; + } + + let logoutDivider = null; + if (logout) { + logoutDivider = <li className='divider'/>; + } + return ( <Dropdown id='sidebar-header-dropdown' @@ -460,56 +581,27 @@ export default class SidebarHeaderDropdown extends React.Component { onClick={this.toggleDropdown} /> <Dropdown.Menu> - <li> - <a - id='accountSettings' - href='#' - onClick={this.toggleAccountSettingsModal} - > - <FormattedMessage - id='navbar_dropdown.accountSettings' - defaultMessage='Account Settings' - /> - </a> - </li> + {accountSettings} + {inviteDivider} {inviteLink} {teamLink} - <li> - <a - id='logout' - href='#' - onClick={() => GlobalActions.emitUserLoggedOutEvent()} - > - <FormattedMessage - id='navbar_dropdown.logout' - defaultMessage='Logout' - /> - </a> - </li> - <li className='divider'/> - {integrationsLink} - {this.renderCustomEmojiLink()} - <li className='divider'/> + {addMemberToTeam} + {teamDivider} {teamSettings} {manageLink} - {sysAdminLink} {teams} - <li className='divider'/> + {backstageDivider} + {integrationsLink} + {customEmoji} + {sysAdminDivider} + {sysAdminLink} + {helpDivider} {helpLink} {reportLink} - <li> - <a - href='#' - onClick={this.handleAboutModal} - > - <FormattedMessage - id='navbar_dropdown.about' - defaultMessage='About Mattermost' - /> - </a> - </li> - {nativeAppDivider} {nativeAppLink} + {about} + {logoutDivider} + {logout} <UserSettingsModal show={this.state.showUserSettingsModal} onModalDismissed={() => this.setState({showUserSettingsModal: false})} @@ -519,6 +611,7 @@ export default class SidebarHeaderDropdown extends React.Component { show={this.state.showAboutModal} onModalDismissed={this.aboutModalDismissed} /> + {addUsersToTeamModal} </Dropdown.Menu> </Dropdown> ); diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index c3c6d0f8f..7e689d95e 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -6,6 +6,7 @@ import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import AboutBuildModal from './about_build_modal.jsx'; +import AddUsersToTeam from './add_users_to_team.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -36,6 +37,8 @@ export default class SidebarRightMenu extends React.Component { this.onChange = this.onChange.bind(this); this.handleClick = this.handleClick.bind(this); this.handleAboutModal = this.handleAboutModal.bind(this); + this.showAddUsersToTeamModal = this.showAddUsersToTeamModal.bind(this); + this.hideAddUsersToTeamModal = this.hideAddUsersToTeamModal.bind(this); this.searchMentions = this.searchMentions.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.getFlagged = this.getFlagged.bind(this); @@ -43,6 +46,7 @@ export default class SidebarRightMenu extends React.Component { const state = this.getStateFromStores(); state.showUserSettingsModal = false; state.showAboutModal = false; + state.showAddUsersToTeamModal = false; this.state = state; } @@ -62,6 +66,21 @@ export default class SidebarRightMenu extends React.Component { this.setState({showAboutModal: false}); } + showAddUsersToTeamModal(e) { + e.preventDefault(); + + this.setState({ + showAddUsersToTeamModal: true, + showDropdown: false + }); + } + + hideAddUsersToTeamModal() { + this.setState({ + showAddUsersToTeamModal: false + }); + } + getFlagged(e) { e.preventDefault(); getFlaggedPosts(); @@ -145,6 +164,7 @@ export default class SidebarRightMenu extends React.Component { const currentUser = UserStore.getCurrentUser(); let teamLink; let inviteLink; + let addUserToTeamLink; let teamSettingsLink; let manageLink; let consoleLink; @@ -165,7 +185,23 @@ export default class SidebarRightMenu extends React.Component { <i className='icon fa fa-user-plus'/> <FormattedMessage id='sidebar_right_menu.inviteNew' - defaultMessage='Invite New Member' + defaultMessage='Send Email Invite' + /> + </a> + </li> + ); + + addUserToTeamLink = ( + <li> + <a + id='addUsersToTeam' + href='#' + onClick={this.showAddUsersToTeamModal} + > + <i className='icon fa fa-user-plus'/> + <FormattedMessage + id='sidebar_right_menu.addMemberToTeam' + defaultMessage='Add Members to Team' /> </a> </li> @@ -192,9 +228,11 @@ export default class SidebarRightMenu extends React.Component { if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; + addUserToTeamLink = null; } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; + addUserToTeamLink = null; } } @@ -360,6 +398,25 @@ export default class SidebarRightMenu extends React.Component { ); } + let addUsersToTeamModal; + if (this.state.showAddUsersToTeamModal) { + addUsersToTeamModal = ( + <AddUsersToTeam + onModalDismissed={this.hideAddUsersToTeamModal} + /> + ); + } + + let teamDivider = null; + if (teamSettingsLink || manageLink || joinAnotherTeamLink) { + teamDivider = <li className='divider'/>; + } + + let consoleDivider = null; + if (consoleLink) { + consoleDivider = <li className='divider'/>; + } + return ( <div className='sidebar--menu' @@ -414,16 +471,20 @@ export default class SidebarRightMenu extends React.Component { /> </a> </li> + <li className='divider'/> {inviteLink} {teamLink} - {joinAnotherTeamLink} - <li className='divider'/> + {addUserToTeamLink} + {teamDivider} {teamSettingsLink} {manageLink} + {joinAnotherTeamLink} + {consoleDivider} {consoleLink} <li className='divider'/> {helpLink} {reportLink} + {nativeAppLink} <li> <a href='#' @@ -437,8 +498,6 @@ export default class SidebarRightMenu extends React.Component { </a> </li> <li className='divider'/> - {nativeAppLink} - <li className='divider'/> <li> <a href='#' @@ -461,6 +520,7 @@ export default class SidebarRightMenu extends React.Component { show={this.state.showAboutModal} onModalDismissed={this.aboutModalDismissed} /> + {addUsersToTeamModal} </div> ); } diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 37207b279..7515c52fd 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -137,6 +137,7 @@ "add_outgoing_webhook.triggerWordsTriggerWhen.help": "Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.", "add_outgoing_webhook.triggerWordsTriggerWhenFullWord": "First word matches a trigger word exactly", "add_outgoing_webhook.triggerWordsTriggerWhenStartsWith": "First word starts with a trigger word", + "add_users_to_team.title": "Add New Members To {teamName} Team", "admin.advance.cluster": "High Availability (Beta)", "admin.advance.metrics": "Performance Monitoring", "admin.audits.reload": "Reload User Activity Logs", @@ -1759,12 +1760,13 @@ "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "About Mattermost", "navbar_dropdown.accountSettings": "Account Settings", + "navbar_dropdown.addMemberToTeam": "Add Members to Team", "navbar_dropdown.console": "System Console", "navbar_dropdown.create": "Create a New Team", "navbar_dropdown.emoji": "Custom Emoji", "navbar_dropdown.help": "Help", "navbar_dropdown.integrations": "Integrations", - "navbar_dropdown.inviteMember": "Invite New Member", + "navbar_dropdown.inviteMember": "Send Email Invite", "navbar_dropdown.join": "Join Another Team", "navbar_dropdown.leave": "Leave Team", "navbar_dropdown.logout": "Logout", @@ -1917,10 +1919,11 @@ "sidebar.unreadBelow": "Unread post(s) below", "sidebar_header.tutorial": "<h4>Main Menu</h4><p>The <strong>Main Menu</strong> is where you can <strong>Invite New Members</strong>, access your <strong>Account Settings</strong> and set your <strong>Theme Color</strong>.</p><p>Team administrators can also access their <strong>Team Settings</strong> from this menu.</p><p>System administrators will find a <strong>System Console</strong> option to administrate the entire system.</p>", "sidebar_right_menu.accountSettings": "Account Settings", + "sidebar_right_menu.addMemberToTeam": "Add Members to Team", "sidebar_right_menu.console": "System Console", "sidebar_right_menu.flagged": "Flagged Posts", "sidebar_right_menu.help": "Help", - "sidebar_right_menu.inviteNew": "Invite New Member", + "sidebar_right_menu.inviteNew": "Send Email Invite", "sidebar_right_menu.logout": "Logout", "sidebar_right_menu.manageMembers": "Manage Members", "sidebar_right_menu.nativeApps": "Download Apps", diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index 6f81a9345..a77527d37 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -252,6 +252,12 @@ class TeamStoreClass extends EventEmitter { } } + removeMemberNotInTeam(teamId = this.getCurrentId(), userId) { + if (this.members_not_in_team[teamId]) { + Reflect.deleteProperty(this.members_not_in_team[teamId], userId); + } + } + getMembersInTeam(teamId = this.getCurrentId()) { return Object.assign({}, this.members_in_team[teamId]) || {}; } @@ -365,6 +371,10 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { TeamStore.saveMyTeam(action.team); TeamStore.emitChange(); break; + case ActionTypes.RECEIVED_TEAM: + TeamStore.saveTeam(action.team); + TeamStore.emitChange(); + break; case ActionTypes.CREATED_TEAM: TeamStore.saveTeam(action.team); TeamStore.appendMyTeamMember(action.member); diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 6f4acae0a..02adeb789 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -15,6 +15,7 @@ const UserStatuses = Constants.UserStatuses; const CHANGE_EVENT_NOT_IN_CHANNEL = 'change_not_in_channel'; const CHANGE_EVENT_IN_CHANNEL = 'change_in_channel'; +const CHANGE_EVENT_NOT_IN_TEAM = 'change_not_in_team'; const CHANGE_EVENT_IN_TEAM = 'change_in_team'; const CHANGE_EVENT_WITHOUT_TEAM = 'change_without_team'; const CHANGE_EVENT = 'change'; @@ -37,6 +38,11 @@ class UserStoreClass extends EventEmitter { this.paging_count = 0; // Lists of sorted IDs for users in a team + this.profiles_not_in_team = {}; + this.not_in_team_offset = 0; + this.not_in_team_count = 0; + + // Lists of sorted IDs for users in a team this.profiles_in_team = {}; this.in_team_offset = 0; this.in_team_count = 0; @@ -85,6 +91,18 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_IN_TEAM, callback); } + emitNotInTeamChange() { + this.emit(CHANGE_EVENT_NOT_IN_TEAM); + } + + addNotInTeamChangeListener(callback) { + this.on(CHANGE_EVENT_NOT_IN_TEAM, callback); + } + + removeNotInTeamChangeListener(callback) { + this.removeListener(CHANGE_EVENT_NOT_IN_TEAM, callback); + } + emitInChannelChange() { this.emit(CHANGE_EVENT_IN_CHANNEL); } @@ -373,6 +391,75 @@ class UserStoreClass extends EventEmitter { userIds.splice(index, 1); } + // Not In Team Profiles + + saveProfilesNotInTeam(teamId, profiles) { + const oldProfileList = this.profiles_not_in_team[teamId] || []; + const oldProfileMap = {}; + for (let i = 0; i < oldProfileList.length; i++) { + oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); + } + + const newProfileMap = Object.assign({}, oldProfileMap, profiles); + const newProfileList = Object.keys(newProfileMap); + + newProfileList.sort((a, b) => { + const aProfile = newProfileMap[a]; + const bProfile = newProfileMap[b]; + + if (aProfile.username < bProfile.username) { + return -1; + } + if (aProfile.username > bProfile.username) { + return 1; + } + return 0; + }); + + this.profiles_not_in_team[teamId] = newProfileList; + this.saveProfiles(profiles); + } + + removeProfileNotInTeam(teamId, userId) { + const userIds = this.profiles_not_in_team[teamId]; + if (!userIds) { + return; + } + + const index = userIds.indexOf(userId); + if (index === -1) { + return; + } + + userIds.splice(index, 1); + } + + getProfileListNotInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) { + const userIds = this.profiles_not_in_team[teamId] || []; + const profiles = []; + const currentId = this.getCurrentId(); + + for (let i = 0; i < userIds.length; i++) { + const profile = this.getProfile(userIds[i]); + + if (!profile) { + continue; + } + + if (skipCurrent && profile.id === currentId) { + continue; + } + + if (skipInactive && profile.delete_at > 0) { + continue; + } + + profiles.push(profile); + } + + return profiles; + } + // Channel-Wide Profiles saveProfilesInChannel(channelId = ChannelStore.getCurrentId(), profiles) { @@ -646,6 +733,19 @@ class UserStoreClass extends EventEmitter { return this.in_team_count; } + setNotInTeamPage(offset, count) { + this.not_in_team_offset = offset + count; + this.not_in_team_count = this.not_in_team_count + count; + } + + getNotInTeamPagingOffset() { + return this.not_in_team_offset; + } + + getNotInTeamPagingCount() { + return this.not_in_team_count; + } + setInChannelPage(channelId, offset, count) { this.in_channel_offset[channelId] = offset + count; this.in_channel_count[channelId] = this.dm_paging_count + count; @@ -694,6 +794,13 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { } UserStore.emitInTeamChange(); break; + case ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM: + UserStore.saveProfilesNotInTeam(action.team_id, action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setNotInTeamPage(action.offset, action.count); + } + UserStore.emitNotInTeamChange(); + break; case ActionTypes.RECEIVED_PROFILES_IN_CHANNEL: UserStore.saveProfilesInChannel(action.channel_id, action.profiles); if (action.offset != null && action.count != null) { diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index b4b361cb4..faaf3aee6 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -403,6 +403,36 @@ export function getProfilesInTeam(teamId = TeamStore.getCurrentId(), offset = Us ); } +export function getProfilesNotInTeam(teamId = TeamStore.getCurrentId(), offset = UserStore.getInTeamPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + const callName = `getProfilesNotInTeam${teamId}${offset}${limit}`; + + if (isCallInProgress(callName)) { + return; + } + + callTracker[callName] = utils.getTimestamp(); + Client.getProfilesNotInTeam( + teamId, + offset, + limit, + (data) => { + callTracker[callName] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM, + profiles: data, + team_id: teamId, + offset, + count: Object.keys(data).length + }); + }, + (err) => { + callTracker[callName] = 0; + dispatchError(err, 'getProfilesNotInTeam'); + } + ); +} + export function getProfilesInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { const callName = `getProfilesInChannel${channelId}${offset}${limit}`; @@ -830,34 +860,6 @@ export function getTeamMember(teamId, userId) { ); } -export function getMyTeamMembers() { - const callName = 'getMyTeamMembers'; - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getMyTeamMembers( - (data) => { - callTracker[callName] = 0; - - const members = {}; - for (const member of data) { - members[member.team_id] = member; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS_UNREAD, - team_members: members - }); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getMyTeamMembers'); - } - ); -} - export function getMyTeamsUnread(teamId) { const members = TeamStore.getMyTeamMembers(); if (members.length > 1) { diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 5b4ab6611..8428f7121 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -95,6 +95,7 @@ export const ActionTypes = keyMirror({ RECEIVED_PROFILES: null, RECEIVED_PROFILES_IN_TEAM: null, + RECEIVED_PROFILES_NOT_IN_TEAM: null, RECEIVED_PROFILE: null, RECEIVED_PROFILES_IN_CHANNEL: null, RECEIVED_PROFILES_NOT_IN_CHANNEL: null, @@ -137,6 +138,7 @@ export const ActionTypes = keyMirror({ RECEIVED_MSG: null, + RECEIVED_TEAM: null, RECEIVED_MY_TEAM: null, CREATED_TEAM: null, UPDATE_TEAM: null, @@ -219,6 +221,7 @@ export const SocketEvents = { CHANNEL_VIEWED: 'channel_viewed', DIRECT_ADDED: 'direct_added', NEW_USER: 'new_user', + ADDED_TO_TEAM: 'added_to_team', LEAVE_TEAM: 'leave_team', UPDATE_TEAM: 'update_team', USER_ADDED: 'user_added', |