summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorGeorge Goldberg <george@gberg.me>2017-04-04 20:17:15 +0100
committerChristopher Speller <crspeller@gmail.com>2017-04-04 15:17:15 -0400
commit1fa3f2351c98e4d1b9c198e357d90ac0d436dcaa (patch)
tree23ff5a64041ed6aa1dc6b7a1db85b85972b2ec66 /webapp
parent77a76487a8e15084c8b5e8e350eb8dc7a87455ea (diff)
downloadchat-1fa3f2351c98e4d1b9c198e357d90ac0d436dcaa.tar.gz
chat-1fa3f2351c98e4d1b9c198e357d90ac0d436dcaa.tar.bz2
chat-1fa3f2351c98e4d1b9c198e357d90ac0d436dcaa.zip
PLT-6023: Add Users to Team in WebApp. (#5956)
* PLT-6198: Use added to channel system message on default channels. Use a different sytem message when a user was added to a default channel by someone else than when they joined themselves. * PLT-6023: Add Users to Team in WebApp. * Fix string text. * Handle added_to_team websocket message. * Fix unread flag on new channel.
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/team_actions.jsx25
-rw-r--r--webapp/actions/user_actions.jsx22
-rw-r--r--webapp/actions/websocket_actions.jsx25
-rw-r--r--webapp/client/client.jsx69
-rw-r--r--webapp/components/add_users_to_team.jsx270
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx185
-rw-r--r--webapp/components/sidebar_right_menu.jsx70
-rwxr-xr-xwebapp/i18n/en.json7
-rw-r--r--webapp/stores/team_store.jsx10
-rw-r--r--webapp/stores/user_store.jsx107
-rw-r--r--webapp/utils/async_client.jsx58
-rw-r--r--webapp/utils/constants.jsx3
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',