summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/cli_test.go37
-rw-r--r--api/team.go115
-rw-r--r--api/team_test.go35
-rw-r--r--i18n/en.json25
-rw-r--r--mattermost.go52
-rw-r--r--model/client.go32
-rw-r--r--model/message.go1
-rw-r--r--model/team_member.go7
-rw-r--r--store/sql_session_store.go24
-rw-r--r--store/sql_team_store.go1
-rw-r--r--webapp/actions/global_actions.jsx7
-rw-r--r--webapp/actions/websocket_actions.jsx17
-rw-r--r--webapp/components/admin_console/team_users.jsx4
-rw-r--r--webapp/components/admin_console/user_item.jsx35
-rw-r--r--webapp/components/filtered_user_list.jsx15
-rw-r--r--webapp/components/leave_team_modal.jsx115
-rw-r--r--webapp/components/navbar_dropdown.jsx14
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/team_members_dropdown.jsx36
-rw-r--r--webapp/i18n/en.json2
-rw-r--r--webapp/package.json2
-rw-r--r--webapp/stores/modal_store.jsx1
-rw-r--r--webapp/stores/team_store.jsx10
-rw-r--r--webapp/utils/constants.jsx2
24 files changed, 572 insertions, 19 deletions
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
@@ -1264,6 +1264,10 @@
"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 (
<UserItem
team={this.state.team}
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index edded5aab..e6c4f637c 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -18,6 +18,7 @@ export default class UserItem 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);
@@ -56,6 +57,19 @@ export default class UserItem extends React.Component {
}
}
+ handleRemoveFromTeam() {
+ Client.removeUserFromTeam(
+ this.props.team.id,
+ this.props.user.id,
+ () => {
+ 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 = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleRemoveFromTeam}
+ >
+ <FormattedMessage
+ id='team_members_dropdown.leave_team'
+ defaultMessage='Remove From Team'
+ />
+ </a>
+ </li>
+ );
+ }
+
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 (
+ <Modal
+ className='modal-confirm'
+ show={this.state.show}
+ onHide={this.handleHide}
+ >
+ <Modal.Header closeButton={false}>
+ <Modal.Title>
+ <FormattedMessage
+ id='leave_team_modal.title'
+ defaultMessage='Leave the team?'
+ />
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <FormattedMessage
+ id='leave_team_modal.desc'
+ defaultMessage='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?'
+ />
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.handleHide}
+ >
+ <FormattedMessage
+ id='leave_team_modal.no'
+ defaultMessage='No'
+ />
+ </button>
+ <button
+ type='button'
+ className='btn btn-danger'
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='leave_team_modal.yes'
+ defaultMessage='Yes'
+ />
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+
+ 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(
+ <li key='leaveTeam_li'>
+ <a
+ href='#'
+ onClick={GlobalActions.showLeaveTeamModal}
+ >
+ <FormattedMessage
+ id='navbar_dropdown.leave'
+ defaultMessage='Leave Team'
+ />
+ </a>
+ </li>
+ );
+
if (this.state.teamMembers && this.state.teamMembers.length > 1) {
teams.push(
<li
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 19ad38887..07b90636d 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -34,6 +34,7 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
import RegisterAppModal from 'components/register_app_modal.jsx';
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from 'components/invite_member_modal.jsx';
+import LeaveTeamModal from 'components/leave_team_modal.jsx';
import SelectTeamModal from 'components/admin_console/select_team_modal.jsx';
export default class NeedsTeam extends React.Component {
@@ -129,6 +130,7 @@ export default class NeedsTeam extends React.Component {
<GetPublicLinkModal/>
<GetTeamInviteLinkModal/>
<InviteMemberModal/>
+ <LeaveTeamModal/>
<ImportThemeModal/>
<TeamSettingsModal/>
<MoreChannelsModal/>
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 = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleRemoveFromTeam}
+ >
+ <FormattedMessage
+ id='team_members_dropdown.leave_team'
+ defaultMessage='Remove From Team'
+ />
+ </a>
+ </li>
+ );
+ }
+
let makeActive = null;
if (showMakeActive) {
// makeActive = (
@@ -260,7 +293,7 @@ export default class TeamMembersDropdown extends React.Component {
// </li>
// );
}
- 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',