From d5f243dad694d6746ec2b6560a81212a78d8c975 Mon Sep 17 00:00:00 2001 From: Corey Hulen Date: Wed, 6 Jul 2016 13:40:59 -0800 Subject: PLT-2863 adding remove user from team (#3429) * PLT-2863 adding remove user from team * PLT-2863 adding the client side UI * Fixing trailing space * Fixing reported issues * Adding documentatino * Switching to final javascript driver --- api/cli_test.go | 37 ++++++++ api/team.go | 115 ++++++++++++++++++++++++- api/team_test.go | 35 +++++++- i18n/en.json | 25 ++++++ mattermost.go | 52 ++++++++++- model/client.go | 32 ++++++- model/message.go | 1 + model/team_member.go | 7 +- store/sql_session_store.go | 24 +++++- store/sql_team_store.go | 1 + webapp/actions/global_actions.jsx | 7 ++ webapp/actions/websocket_actions.jsx | 17 ++++ webapp/components/admin_console/team_users.jsx | 4 + webapp/components/admin_console/user_item.jsx | 35 +++++++- webapp/components/filtered_user_list.jsx | 15 +++- webapp/components/leave_team_modal.jsx | 115 +++++++++++++++++++++++++ webapp/components/navbar_dropdown.jsx | 14 +++ webapp/components/needs_team.jsx | 2 + webapp/components/team_members_dropdown.jsx | 36 +++++++- webapp/i18n/en.json | 2 + webapp/package.json | 2 +- webapp/stores/modal_store.jsx | 1 + webapp/stores/team_store.jsx | 10 +++ webapp/utils/constants.jsx | 2 + 24 files changed, 572 insertions(+), 19 deletions(-) create mode 100644 webapp/components/leave_team_modal.jsx diff --git a/api/cli_test.go b/api/cli_test.go index 8184c2e06..ae2abee4a 100644 --- a/api/cli_test.go +++ b/api/cli_test.go @@ -334,6 +334,43 @@ func TestCliJoinTeam(t *testing.T) { } } +func TestCliLeaveTeam(t *testing.T) { + if disableCliTests { + return + } + + th := Setup().InitBasic() + + cmd := exec.Command("bash", "-c", `go run ../mattermost.go -leave_team -team_name="`+th.BasicTeam.Name+`" -email="`+th.BasicUser.Email+`"`) + output, err := cmd.CombinedOutput() + if err != nil { + t.Log(string(output)) + t.Fatal(err) + } + + profiles := th.BasicClient.Must(th.BasicClient.GetProfiles(th.BasicTeam.Id, "")).Data.(map[string]*model.User) + + found := false + + for _, user := range profiles { + if user.Email == th.BasicUser.Email { + found = true + } + + } + + if !found { + t.Fatal("profile still should be in team even if deleted") + } + + if result := <-Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { + teamMembers := result.Data.([]*model.TeamMember) + if len(teamMembers) > 0 { + t.Fatal("Shouldn't be in team") + } + } +} + func TestCliResetPassword(t *testing.T) { if disableCliTests { return diff --git a/api/team.go b/api/team.go index 50e32e625..7f8a421ce 100644 --- a/api/team.go +++ b/api/team.go @@ -17,7 +17,6 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -39,6 +38,7 @@ func InitTeam() { BaseRoutes.NeedTeam.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") BaseRoutes.NeedTeam.Handle("/add_user_to_team", ApiUserRequired(addUserToTeam)).Methods("POST") + BaseRoutes.NeedTeam.Handle("/remove_user_from_team", ApiUserRequired(removeUserFromTeam)).Methods("POST") // These should be moved to the global admin console BaseRoutes.NeedTeam.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") @@ -266,11 +266,23 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { channelRole = model.CHANNEL_ROLE_ADMIN } - if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { - if tmr.Err.Id == store.TEAM_MEMBER_EXISTS_ERROR { + if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil { + // Membership alredy exists. Check if deleted and and update, otherwise do nothing + rtm := etmr.Data.(model.TeamMember) + + // Do nothing if already added + if rtm.DeleteAt == 0 { return nil } - return tmr.Err + + if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { + return tmr.Err + } + } else { + // Membership appears to be missing. Lets try to add. + if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { + return tmr.Err + } } if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { @@ -291,6 +303,56 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { return nil } +func LeaveTeam(team *model.Team, user *model.User) *model.AppError { + + var teamMember model.TeamMember + + if result := <-Srv.Store.Team().GetMember(team.Id, user.Id); result.Err != nil { + return model.NewLocAppError("RemoveUserFromTeam", "api.team.remove_user_from_team.missing.app_error", nil, result.Err.Error()) + } else { + teamMember = result.Data.(model.TeamMember) + } + + var channelMembers *model.ChannelList + + if result := <-Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil { + if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" { + channelMembers = &model.ChannelList{make([]*model.Channel, 0), make(map[string]*model.ChannelMember)} + } else { + return result.Err + } + + } else { + channelMembers = result.Data.(*model.ChannelList) + } + + for _, channel := range channelMembers.Channels { + if channel.Type != model.CHANNEL_DIRECT { + if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil { + return result.Err + } + } + } + + teamMember.Roles = "" + teamMember.DeleteAt = model.GetMillis() + + if result := <-Srv.Store.Team().UpdateMember(&teamMember); result.Err != nil { + return result.Err + } + + if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { + return uua.Err + } + + RemoveAllSessionsForUserId(user.Id) + InvalidateCacheForUser(user.Id) + + go Publish(model.NewMessage(team.Id, "", user.Id, model.ACTION_LEAVE_TEAM)) + + return nil +} + func isTeamCreationAllowed(c *Context, email string) bool { email = strings.ToLower(email) @@ -483,6 +545,51 @@ func addUserToTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(params))) } +func removeUserFromTeam(c *Context, w http.ResponseWriter, r *http.Request) { + params := model.MapFromJson(r.Body) + userId := params["user_id"] + + if len(userId) != 26 { + c.SetInvalidParam("removeUserFromTeam", "user_id") + return + } + + tchan := Srv.Store.Team().Get(c.TeamId) + uchan := Srv.Store.User().Get(userId) + + var team *model.Team + if result := <-tchan; result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if c.Session.UserId != user.Id { + if !c.IsTeamAdmin() { + c.Err = model.NewLocAppError("removeUserFromTeam", "api.team.update_team.permissions.app_error", nil, "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + } + + err := LeaveTeam(team, user) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.MapToJson(params))) +} + func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) { params := model.MapFromJson(r.Body) diff --git a/api/team_test.go b/api/team_test.go index a62ffcdb5..9fc3e8105 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -158,7 +158,7 @@ func TestAddUserToTeam(t *testing.T) { } user2 := th.CreateUser(th.BasicClient) - if result, err := th.BasicClient.AddUserToTeam(user2.Id); err != nil { + if result, err := th.BasicClient.AddUserToTeam("", user2.Id); err != nil { t.Fatal(err) } else { rm := result.Data.(map[string]string) @@ -168,6 +168,39 @@ func TestAddUserToTeam(t *testing.T) { } } +func TestRemoveUserFromTeam(t *testing.T) { + th := Setup().InitSystemAdmin().InitBasic() + + if _, err := th.BasicClient.RemoveUserFromTeam(th.SystemAdminTeam.Id, th.SystemAdminUser.Id); err == nil { + t.Fatal("should fail not enough permissions") + } else { + if err.Id != "api.context.permissions.app_error" { + t.Fatal("wrong error") + } + } + + if _, err := th.BasicClient.RemoveUserFromTeam("", th.SystemAdminUser.Id); err == nil { + t.Fatal("should fail not enough permissions") + } else { + if err.Id != "api.team.update_team.permissions.app_error" { + t.Fatal("wrong error") + } + } + + if _, err := th.BasicClient.RemoveUserFromTeam("", th.BasicUser.Id); err != nil { + t.Fatal("should have removed the user from the team") + } + + th.BasicClient.Logout() + th.LoginSystemAdmin() + + th.SystemAdminClient.Must(th.SystemAdminClient.AddUserToTeam(th.BasicTeam.Id, th.BasicUser.Id)) + + if _, err := th.SystemAdminClient.RemoveUserFromTeam(th.BasicTeam.Id, th.BasicUser.Id); err != nil { + t.Fatal("should have removed the user from the team") + } +} + func TestAddUserToTeamFromInvite(t *testing.T) { th := Setup().InitBasic() th.BasicClient.Logout() diff --git a/i18n/en.json b/i18n/en.json index 8bd66522d..78f56986b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1263,6 +1263,10 @@ "id": "api.slackimport.slack_import.zip.app_error", "translation": "Unable to open zip file" }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "The user does not appear to be part of this team." + }, { "id": "api.team.create_team.email_disabled.app_error", "translation": "Team sign-up with email is disabled." @@ -4223,6 +4227,27 @@ "id": "utils.mail.test.configured.error", "translation": "SMTP server settings do not appear to be configured properly err=%v details=%v" }, + + { + "id": "utils.mail.test.configured.error", + "translation": "SMTP server settings do not appear to be configured properly err=%v details=%v" + }, + { + "id": "leave_team_modal.title", + "translation": "Leave the team?" + }, + { + "id": "leave_team_modal.desc", + "translation": "You will be removed from all public channels and private groups. If the team is private you will not be able to rejoin the team. Are you sure?" + }, + { + "id": "leave_team_modal.no", + "translation": "No" + }, + { + "id": "leave_team_modal.yes", + "translation": "Yes" + }, { "id": "web.admin_console.title", "translation": "Admin Console" diff --git a/mattermost.go b/mattermost.go index 423e00536..14f297a66 100644 --- a/mattermost.go +++ b/mattermost.go @@ -50,6 +50,7 @@ var flagCmdLeaveChannel bool var flagCmdListChannels bool var flagCmdRestoreChannel bool var flagCmdJoinTeam bool +var flagCmdLeaveTeam bool var flagCmdVersion bool var flagCmdRunWebClientTests bool var flagCmdRunJavascriptClientTests bool @@ -286,6 +287,7 @@ func parseCmds() { flag.BoolVar(&flagCmdListChannels, "list_channels", false, "") flag.BoolVar(&flagCmdRestoreChannel, "restore_channel", false, "") flag.BoolVar(&flagCmdJoinTeam, "join_team", false, "") + flag.BoolVar(&flagCmdLeaveTeam, "leave_team", false, "") flag.BoolVar(&flagCmdVersion, "version", false, "") flag.BoolVar(&flagCmdRunWebClientTests, "run_web_client_tests", false, "") flag.BoolVar(&flagCmdRunJavascriptClientTests, "run_javascript_client_tests", false, "") @@ -303,6 +305,7 @@ func parseCmds() { flagRunCmds = (flagCmdCreateTeam || flagCmdCreateUser || flagCmdInviteUser || + flagCmdLeaveTeam || flagCmdAssignRole || flagCmdJoinChannel || flagCmdLeaveChannel || @@ -328,6 +331,7 @@ func runCmds() { cmdCreateTeam() cmdCreateUser() cmdInviteUser() + cmdLeaveTeam() cmdAssignRole() cmdJoinChannel() cmdLeaveChannel() @@ -1187,6 +1191,47 @@ func cmdJoinTeam() { } } +func cmdLeaveTeam() { + if flagCmdLeaveTeam { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + flag.Usage() + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + flag.Usage() + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + err := api.LeaveTeam(team, user) + + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + func cmdResetPassword() { if flagCmdResetPassword { if len(flagEmail) == 0 { @@ -1517,7 +1562,12 @@ COMMANDS: Example: platform -invite_user -team_name="name" -email="user@example.com" -site_url="https://mattermost.example.com" - -join_team Joins a user to the team. It requires the -email and +-leave_team Removes a user from a team. It requires the -team_name + and -email. + Example: + platform -remove_user_from_team -team_name="name" -email="user@example.com" + + -join_team Joins a user to the team. It required the -email and -team_name. You may need to logout of your current session for the new team to be applied. Example: diff --git a/model/client.go b/model/client.go index 1882fd0ab..2f1e846c2 100644 --- a/model/client.go +++ b/model/client.go @@ -345,10 +345,18 @@ func (c *Client) FindTeamByName(name string) (*Result, *AppError) { } } -func (c *Client) AddUserToTeam(userId string) (*Result, *AppError) { +// Adds a user directly to the team without sending an invite. +// The teamId and userId are required. You must be a valid member of the team and/or +// have the correct role to add new users to the team. Returns a map of user_id=userId +// if successful, otherwise returns an AppError. +func (c *Client) AddUserToTeam(teamId string, userId string) (*Result, *AppError) { + if len(teamId) == 0 { + teamId = c.GetTeamId() + } + data := make(map[string]string) data["user_id"] = userId - if r, err := c.DoApiPost(c.GetTeamRoute()+"/add_user_to_team", MapToJson(data)); err != nil { + if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/add_user_to_team", MapToJson(data)); err != nil { return nil, err } else { defer closeBody(r) @@ -371,6 +379,26 @@ func (c *Client) AddUserToTeamFromInvite(hash, dataToHash, inviteId string) (*Re } } +// Removes a user directly from the team. +// The teamId and userId are required. You must be a valid member of the team and/or +// have the correct role to remove a user from the team. Returns a map of user_id=userId +// if successful, otherwise returns an AppError. +func (c *Client) RemoveUserFromTeam(teamId string, userId string) (*Result, *AppError) { + if len(teamId) == 0 { + teamId = c.GetTeamId() + } + + data := make(map[string]string) + data["user_id"] = userId + if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/remove_user_from_team", MapToJson(data)); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { if r, err := c.DoApiPost(c.GetTeamRoute()+"/invite_members", invites.ToJson()); err != nil { return nil, err diff --git a/model/message.go b/model/message.go index a986af4de..12f3be663 100644 --- a/model/message.go +++ b/model/message.go @@ -17,6 +17,7 @@ const ( ACTION_CHANNEL_VIEWED = "channel_viewed" ACTION_DIRECT_ADDED = "direct_added" ACTION_NEW_USER = "new_user" + ACTION_LEAVE_TEAM = "leave_team" ACTION_USER_ADDED = "user_added" ACTION_USER_REMOVED = "user_removed" ACTION_PREFERENCE_CHANGED = "preference_changed" diff --git a/model/team_member.go b/model/team_member.go index ae687c109..7d932dec4 100644 --- a/model/team_member.go +++ b/model/team_member.go @@ -14,9 +14,10 @@ const ( ) type TeamMember struct { - TeamId string `json:"team_id"` - UserId string `json:"user_id"` - Roles string `json:"roles"` + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + DeleteAt int64 `json:"delete_at"` } func (o *TeamMember) ToJson() string { diff --git a/store/sql_session_store.go b/store/sql_session_store.go index 525d0e5b2..9ad3a5efa 100644 --- a/store/sql_session_store.go +++ b/store/sql_session_store.go @@ -75,7 +75,13 @@ func (me SqlSessionStore) Save(session *model.Session) StoreChannel { result.Err = model.NewLocAppError("SqlSessionStore.Save", "store.sql_session.save.app_error", nil, "id="+session.Id+", "+rtcs.Err.Error()) return } else { - session.TeamMembers = rtcs.Data.([]*model.TeamMember) + tempMembers := rtcs.Data.([]*model.TeamMember) + session.TeamMembers = make([]*model.TeamMember, 0, len(tempMembers)) + for _, tm := range tempMembers { + if tm.DeleteAt == 0 { + session.TeamMembers = append(session.TeamMembers, tm) + } + } } storeChannel <- result @@ -106,7 +112,13 @@ func (me SqlSessionStore) Get(sessionIdOrToken string) StoreChannel { result.Err = model.NewLocAppError("SqlSessionStore.Get", "store.sql_session.get.app_error", nil, "sessionIdOrToken="+sessionIdOrToken+", "+rtcs.Err.Error()) return } else { - sessions[0].TeamMembers = rtcs.Data.([]*model.TeamMember) + tempMembers := rtcs.Data.([]*model.TeamMember) + sessions[0].TeamMembers = make([]*model.TeamMember, 0, len(tempMembers)) + for _, tm := range tempMembers { + if tm.DeleteAt == 0 { + sessions[0].TeamMembers = append(sessions[0].TeamMembers, tm) + } + } } } @@ -144,7 +156,13 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel { return } else { for _, session := range sessions { - session.TeamMembers = rtcs.Data.([]*model.TeamMember) + tempMembers := rtcs.Data.([]*model.TeamMember) + session.TeamMembers = make([]*model.TeamMember, 0, len(tempMembers)) + for _, tm := range tempMembers { + if tm.DeleteAt == 0 { + session.TeamMembers = append(session.TeamMembers, tm) + } + } } } diff --git a/store/sql_team_store.go b/store/sql_team_store.go index c668988dc..ddcaa7896 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -41,6 +41,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { } func (s SqlTeamStore) UpgradeSchemaIfNeeded() { + s.CreateColumnIfNotExists("TeamMembers", "DeleteAt", "bigint(20)", "bigint", "0") } func (s SqlTeamStore) CreateIndexesIfNotExists() { diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 4baed20c3..aa51f6f62 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -288,6 +288,13 @@ export function showInviteMemberModal() { }); } +export function showLeaveTeamModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_LEAVE_TEAM_MODAL, + value: true + }); +} + export function showRegisterAppModal() { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index 17f84638d..9d9cf62b7 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -135,6 +135,10 @@ function handleMessage(msg) { handleNewUserEvent(); break; + case SocketEvents.LEAVE_TEAM: + handleLeaveTeamEvent(msg); + break; + case SocketEvents.USER_ADDED: handleUserAddedEvent(msg); break; @@ -219,6 +223,19 @@ function handleNewUserEvent() { AsyncClient.getChannelExtraInfo(); } +function handleLeaveTeamEvent(msg) { + if (UserStore.getCurrentId() === msg.user_id) { + TeamStore.removeTeamMember(msg.team_id); + + // if the are on the team begin removed redirect them to the root + if (TeamStore.getCurrentId() === msg.team_id) { + browserHistory.push('/'); + } + } else if (TeamStore.getCurrentId() === msg.team_id) { + GlobalActions.emitProfilesForDmList(); + } +} + function handleDirectAddedEvent(msg) { AsyncClient.getChannel(msg.channel_id); AsyncClient.getDirectProfiles(); diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx index b6bba3182..3ec375627 100644 --- a/webapp/components/admin_console/team_users.jsx +++ b/webapp/components/admin_console/team_users.jsx @@ -186,6 +186,10 @@ export default class UserList extends React.Component { var memberList = this.state.users.map((user) => { var teamMember = this.getTeamMemberForUser(user.id); + if (teamMember.delete_at > 0) { + return null; + } + return ( { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + handleMakeActive(e) { e.preventDefault(); Client.updateActive(this.props.user.id, true, @@ -222,6 +236,7 @@ export default class UserItem extends React.Component { ); } + const me = UserStore.getCurrentUser(); const email = user.email; let showMakeMember = teamMember.roles === 'admin' || user.roles === 'system_admin'; let showMakeAdmin = teamMember.roles === '' && user.roles !== 'system_admin'; @@ -299,6 +314,24 @@ export default class UserItem extends React.Component { ); } + let removeFromTeam = null; + if (this.props.user.id !== me.id) { + removeFromTeam = ( +
  • + + + +
  • + ); + } + let makeActive = null; if (showMakeActive) { makeActive = ( @@ -428,7 +461,6 @@ export default class UserItem extends React.Component { passwordReset = null; } - const me = UserStore.getCurrentUser(); let makeDemoteModal = null; if (this.props.user.id === me.id) { const title = ( @@ -511,6 +543,7 @@ export default class UserItem extends React.Component { className='dropdown-menu member-menu' role='menu' > + {removeFromTeam} {makeAdmin} {makeMember} {makeActive} diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx index b6d8f11f9..67d038fd9 100644 --- a/webapp/components/filtered_user_list.jsx +++ b/webapp/components/filtered_user_list.jsx @@ -39,17 +39,24 @@ class FilteredUserList extends React.Component { this.state = { filter: '', users: this.filterUsers(props.teamMembers, props.users), - selected: 'team' + selected: 'team', + teamMembers: props.teamMembers }; } - componentWillUpdate(nextProps) { + componentWillReceiveProps(nextProps) { // assume the user list is immutable if (this.props.users !== nextProps.users) { this.setState({ users: this.filterUsers(nextProps.teamMembers, nextProps.users) }); } + + if (this.props.teamMembers !== nextProps.teamMembers) { + this.setState({ + users: this.filterUsers(nextProps.teamMembers, nextProps.users) + }); + } } componentDidMount() { @@ -70,6 +77,10 @@ class FilteredUserList extends React.Component { var filteredUsers = users.filter((user) => { for (const index in teamMembers) { if (teamMembers.hasOwnProperty(index) && teamMembers[index].user_id === user.id) { + if (teamMembers[index].delete_at > 0) { + return false; + } + return true; } } diff --git a/webapp/components/leave_team_modal.jsx b/webapp/components/leave_team_modal.jsx new file mode 100644 index 000000000..7263f23d4 --- /dev/null +++ b/webapp/components/leave_team_modal.jsx @@ -0,0 +1,115 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import * as GlobalActions from 'actions/global_actions.jsx'; +import ModalStore from 'stores/modal_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {intlShape, injectIntl, FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +import React from 'react'; + +class LeaveTeamModal extends React.Component { + constructor(props) { + super(props); + + this.handleToggle = this.handleToggle.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleHide = this.handleHide.bind(this); + + this.state = { + show: false + }; + } + + componentDidMount() { + ModalStore.addModalListener(ActionTypes.TOGGLE_LEAVE_TEAM_MODAL, this.handleToggle); + } + + componentWillUnmount() { + ModalStore.removeModalListener(ActionTypes.TOGGLE_LEAVE_TEAM_MODAL, this.handleToggle); + } + + handleToggle(value) { + this.setState({ + show: value + }); + } + + handleSubmit() { + GlobalActions.emitLeaveTeam(); + + this.setState({ + show: false + }); + } + + handleHide() { + this.setState({ + show: false + }); + } + + render() { + var currentUser = UserStore.getCurrentUser(); + + if (currentUser != null) { + return ( + + + + + + + + + + + + + + + ); + } + + return null; + } +} + +LeaveTeamModal.propTypes = { + intl: intlShape.isRequired +}; + +export default injectIntl(LeaveTeamModal); diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index ab228dcb3..c660bc164 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -236,6 +236,20 @@ export default class NavbarDropdown extends React.Component { ); } + teams.push( +
  • + + + +
  • + ); + if (this.state.teamMembers && this.state.teamMembers.length > 1) { teams.push(
  • + diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx index 2b40da9cf..43449635d 100644 --- a/webapp/components/team_members_dropdown.jsx +++ b/webapp/components/team_members_dropdown.jsx @@ -19,6 +19,7 @@ export default class TeamMembersDropdown extends React.Component { super(props); this.handleMakeMember = this.handleMakeMember.bind(this); + this.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this); this.handleMakeActive = this.handleMakeActive.bind(this); this.handleMakeNotActive = this.handleMakeNotActive.bind(this); this.handleMakeAdmin = this.handleMakeAdmin.bind(this); @@ -52,6 +53,19 @@ export default class TeamMembersDropdown extends React.Component { ); } } + handleRemoveFromTeam() { + Client.removeUserFromTeam( + '', + this.props.user.id, + () => { + AsyncClient.getTeamMembers(TeamStore.getCurrentId()); + AsyncClient.getProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } handleMakeActive() { Client.updateActive(this.props.user.id, true, () => { @@ -171,6 +185,7 @@ export default class TeamMembersDropdown extends React.Component { ); } + const me = UserStore.getCurrentUser(); let showMakeMember = teamMember.roles === 'admin' || user.roles === 'system_admin'; let showMakeAdmin = teamMember.roles === '' && user.roles !== 'system_admin'; let showMakeActive = false; @@ -225,6 +240,24 @@ export default class TeamMembersDropdown extends React.Component { ); } + let removeFromTeam = null; + if (this.props.user.id !== me.id) { + removeFromTeam = ( +
  • + + + +
  • + ); + } + let makeActive = null; if (showMakeActive) { // makeActive = ( @@ -260,7 +293,7 @@ export default class TeamMembersDropdown extends React.Component { // // ); } - const me = UserStore.getCurrentUser(); + let makeDemoteModal = null; if (this.props.user.id === me.id) { const title = ( @@ -321,6 +354,7 @@ export default class TeamMembersDropdown extends React.Component { className='dropdown-menu member-menu' role='menu' > + {removeFromTeam} {makeAdmin} {makeMember} {makeActive} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 945c9c4a9..322c9ccad 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1202,6 +1202,7 @@ "navbar_dropdown.accountSettings": "Account Settings", "navbar_dropdown.console": "System Console", "navbar_dropdown.create": "Create a New Team", + "navbar_dropdown.leave": "Leave Team", "navbar_dropdown.emoji": "Custom Emoji", "navbar_dropdown.help": "Help", "navbar_dropdown.integrations": "Integrations", @@ -1414,6 +1415,7 @@ "team_members_dropdown.makeAdmin": "Make Team Admin", "team_members_dropdown.makeInactive": "Make Inactive", "team_members_dropdown.makeMember": "Make Member", + "team_members_dropdown.leave_team": "Remove From Team", "team_members_dropdown.member": "Member", "team_members_dropdown.systemAdmin": "System Admin", "team_members_dropdown.teamAdmin": "Team Admin", diff --git a/webapp/package.json b/webapp/package.json index b3066542e..468325e7d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,7 +18,7 @@ "keymirror": "0.1.1", "marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b", "match-at": "0.1.0", - "mattermost": "mattermost/mattermost-javascript#18527e6c4a9aea69aa7845a62d9618b357faa4e7", + "mattermost": "mattermost/mattermost-javascript#c72a75ca4ac135e2d476fc048ef7adc450e6739f", "object-assign": "4.1.0", "perfect-scrollbar": "0.6.11", "react": "15.0.2", diff --git a/webapp/stores/modal_store.jsx b/webapp/stores/modal_store.jsx index 0595daaf9..0209f3993 100644 --- a/webapp/stores/modal_store.jsx +++ b/webapp/stores/modal_store.jsx @@ -33,6 +33,7 @@ class ModalStoreClass extends EventEmitter { switch (type) { case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL: + case ActionTypes.TOGGLE_LEAVE_TEAM_MODAL: case ActionTypes.TOGGLE_DELETE_POST_MODAL: case ActionTypes.TOGGLE_GET_POST_LINK_MODAL: case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL: diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index c35c467ae..f4383589a 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -139,6 +139,16 @@ class TeamStoreClass extends EventEmitter { this.team_members.push(member); } + removeTeamMember(teamId) { + for (var index in this.team_members) { + if (this.team_members.hasOwnProperty(index)) { + if (this.team_members[index].team_id === teamId) { + Reflect.deleteProperty(this.team_members, index); + } + } + } + } + getTeamMembers() { return this.team_members; } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index efae8a050..f0cea9e52 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -113,6 +113,7 @@ export default { TOGGLE_IMPORT_THEME_MODAL: null, TOGGLE_INVITE_MEMBER_MODAL: null, + TOGGLE_LEAVE_TEAM_MODAL: null, TOGGLE_DELETE_POST_MODAL: null, TOGGLE_GET_POST_LINK_MODAL: null, TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null, @@ -160,6 +161,7 @@ export default { CHANNEL_VIEWED: 'channel_viewed', DIRECT_ADDED: 'direct_added', NEW_USER: 'new_user', + LEAVE_TEAM: 'leave_team', USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', TYPING: 'typing', -- cgit v1.2.3-1-g7c22