From 137ade29d061e158543da814ecd0d06d7e992c1f Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Wed, 2 Nov 2016 14:38:34 -0400 Subject: PLT-4535/PLT-4503 Fix inactive users in searches and add option functionality to DB user search (#4413) * Add options to user database search * Fix inactive users showing up incorrectly in some user searches * Read JSON for searchUsers API into anonymous struct * Move anonymous struct to be a normal struct in model directory and upadte client to use it * Added clarification comment about slightly odd query condition in search --- api/user.go | 37 ++++++----- api/user_test.go | 58 ++++++++++++++--- model/client.go | 6 +- model/user_search.go | 39 +++++++++++ model/user_search_test.go | 19 ++++++ store/sql_user_store.go | 79 +++++++++++++--------- store/sql_user_store_test.go | 90 ++++++++++++++++++++++---- store/store.go | 6 +- webapp/components/admin_console/team_users.jsx | 7 +- webapp/components/channel_invite_modal.jsx | 2 +- webapp/components/channel_members_modal.jsx | 2 +- webapp/components/more_direct_channels.jsx | 6 +- webapp/stores/user_store.jsx | 6 +- webapp/utils/constants.jsx | 5 ++ 14 files changed, 279 insertions(+), 83 deletions(-) create mode 100644 model/user_search.go create mode 100644 model/user_search_test.go diff --git a/api/user.go b/api/user.go index 37f8d6818..2c00dd4c8 100644 --- a/api/user.go +++ b/api/user.go @@ -2592,33 +2592,36 @@ func sanitizeProfile(c *Context, user *model.User) *model.User { } func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { - props := model.MapFromJson(r.Body) + props := model.UserSearchFromJson(r.Body) + if props == nil { + c.SetInvalidParam("searchUsers", "") + return + } - term := props["term"] - if len(term) == 0 { + if len(props.Term) == 0 { c.SetInvalidParam("searchUsers", "term") return } - teamId := props["team_id"] - inChannelId := props["in_channel"] - notInChannelId := props["not_in_channel"] - - if inChannelId != "" && !HasPermissionToChannelContext(c, inChannelId, model.PERMISSION_READ_CHANNEL) { + if props.InChannelId != "" && !HasPermissionToChannelContext(c, props.InChannelId, model.PERMISSION_READ_CHANNEL) { return } - if notInChannelId != "" && !HasPermissionToChannelContext(c, notInChannelId, model.PERMISSION_READ_CHANNEL) { + if props.NotInChannelId != "" && !HasPermissionToChannelContext(c, props.NotInChannelId, model.PERMISSION_READ_CHANNEL) { return } + searchOptions := map[string]bool{} + searchOptions[store.USER_SEARCH_OPTION_USERNAME_ONLY] = true + searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = props.AllowInactive + var uchan store.StoreChannel - if inChannelId != "" { - uchan = Srv.Store.User().SearchInChannel(inChannelId, term, store.USER_SEARCH_TYPE_USERNAME) - } else if notInChannelId != "" { - uchan = Srv.Store.User().SearchNotInChannel(teamId, notInChannelId, term, store.USER_SEARCH_TYPE_USERNAME) + if props.InChannelId != "" { + uchan = Srv.Store.User().SearchInChannel(props.InChannelId, props.Term, searchOptions) + } else if props.NotInChannelId != "" { + uchan = Srv.Store.User().SearchNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions) } else { - uchan = Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_USERNAME) + uchan = Srv.Store.User().Search(props.TeamId, props.Term, searchOptions) } if result := <-uchan; result.Err != nil { @@ -2674,8 +2677,8 @@ func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Reque return } - uchan := Srv.Store.User().SearchInChannel(channelId, term, store.USER_SEARCH_TYPE_ALL) - nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, store.USER_SEARCH_TYPE_ALL) + uchan := Srv.Store.User().SearchInChannel(channelId, term, map[string]bool{}) + nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, map[string]bool{}) autocomplete := &model.UserAutocompleteInChannel{} @@ -2720,7 +2723,7 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request) } } - uchan := Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_ALL) + uchan := Srv.Store.User().Search(teamId, term, map[string]bool{}) autocomplete := &model.UserAutocompleteInTeam{} diff --git a/api/user_test.go b/api/user_test.go index 2c6238c54..75e246ab3 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -2034,10 +2034,14 @@ func TestGetProfilesNotInChannel(t *testing.T) { } func TestSearchUsers(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient - if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil { + inactiveUser := th.CreateUser(Client) + LinkUserToTeam(inactiveUser, th.BasicTeam) + th.SystemAdminClient.Must(th.SystemAdminClient.UpdateActive(inactiveUser.Id, false)) + + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2054,7 +2058,41 @@ func TestSearchUsers(t *testing.T) { } } - if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err != nil { + if result, err := Client.SearchUsers(model.UserSearch{Term: inactiveUser.Username, TeamId: th.BasicTeam.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == inactiveUser.Id { + found = true + } + } + + if found { + t.Fatal("should not have found inactive user") + } + } + + if result, err := Client.SearchUsers(model.UserSearch{Term: inactiveUser.Username, TeamId: th.BasicTeam.Id, AllowInactive: true}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == inactiveUser.Id { + found = true + } + } + + if !found { + t.Fatal("should have found inactive user") + } + } + + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, InChannelId: th.BasicChannel.Id}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2075,7 +2113,7 @@ func TestSearchUsers(t *testing.T) { } } - if result, err := Client.SearchUsers(th.BasicUser2.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser2.Username, NotInChannelId: th.BasicChannel.Id}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2102,7 +2140,7 @@ func TestSearchUsers(t *testing.T) { } } - if result, err := Client.SearchUsers(th.BasicUser2.Username, th.BasicTeam.Id, map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser2.Username, TeamId: th.BasicTeam.Id, NotInChannelId: th.BasicChannel.Id}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2129,7 +2167,7 @@ func TestSearchUsers(t *testing.T) { } } - if result, err := Client.SearchUsers(th.BasicUser.Username, "junk", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, TeamId: "junk", NotInChannelId: th.BasicChannel.Id}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2141,7 +2179,7 @@ func TestSearchUsers(t *testing.T) { th.LoginBasic2() - if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil { + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username}); err != nil { t.Fatal(err) } else { users := result.Data.([]*model.User) @@ -2158,15 +2196,15 @@ func TestSearchUsers(t *testing.T) { } } - if _, err := Client.SearchUsers("", "", map[string]string{}); err == nil { + if _, err := Client.SearchUsers(model.UserSearch{}); err == nil { t.Fatal("should have errored - blank term") } - if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err == nil { + if _, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, InChannelId: th.BasicChannel.Id}); err == nil { t.Fatal("should not have access") } - if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err == nil { + if _, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, NotInChannelId: th.BasicChannel.Id}); err == nil { t.Fatal("should not have access") } } diff --git a/model/client.go b/model/client.go index e9d6c512c..02c6ac9b2 100644 --- a/model/client.go +++ b/model/client.go @@ -572,10 +572,8 @@ func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) { // SearchUsers returns a list of users that have a username matching or similar to the search term. Must // be authenticated. -func (c *Client) SearchUsers(term string, teamId string, options map[string]string) (*Result, *AppError) { - options["term"] = term - options["team_id"] = teamId - if r, err := c.DoApiPost("/users/search", MapToJson(options)); err != nil { +func (c *Client) SearchUsers(params UserSearch) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/search", params.ToJson()); err != nil { return nil, err } else { defer closeBody(r) diff --git a/model/user_search.go b/model/user_search.go new file mode 100644 index 000000000..4bbd2bd78 --- /dev/null +++ b/model/user_search.go @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserSearch struct { + Term string `json:"term"` + TeamId string `json:"team_id"` + InChannelId string `json:"in_channel_id"` + NotInChannelId string `json:"not_in_channel_id"` + AllowInactive bool `json:"allow_inactive"` +} + +// ToJson convert a User to a json string +func (u *UserSearch) ToJson() string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +// UserSearchFromJson will decode the input and return a User +func UserSearchFromJson(data io.Reader) *UserSearch { + decoder := json.NewDecoder(data) + var us UserSearch + err := decoder.Decode(&us) + if err == nil { + return &us + } else { + return nil + } +} diff --git a/model/user_search_test.go b/model/user_search_test.go new file mode 100644 index 000000000..b2543ffdb --- /dev/null +++ b/model/user_search_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestUserSearchJson(t *testing.T) { + userSearch := UserSearch{Term: NewId(), TeamId: NewId()} + json := userSearch.ToJson() + ruserSearch := UserSearchFromJson(strings.NewReader(json)) + + if userSearch.Term != ruserSearch.Term { + t.Fatal("Terms do not match") + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 4f5e11d00..17fdcbc85 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -15,12 +15,14 @@ import ( ) const ( - MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const" - MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error" - PROFILES_IN_CHANNEL_CACHE_SIZE = 5000 - PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins - USER_SEARCH_TYPE_ALL = "Username, FirstName, LastName, Nickname" - USER_SEARCH_TYPE_USERNAME = "Username" + MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const" + MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error" + PROFILES_IN_CHANNEL_CACHE_SIZE = 5000 + PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins + USER_SEARCH_OPTION_USERNAME_ONLY = "username_only" + USER_SEARCH_OPTION_ALLOW_INACTIVE = "allow_inactive" + USER_SEARCH_TYPE_ALL = "Username, FirstName, LastName, Nickname" + USER_SEARCH_TYPE_USERNAME = "Username" ) type SqlUserStore struct { @@ -1085,20 +1087,24 @@ func (us SqlUserStore) GetUnreadCountForChannel(userId string, channelId string) return storeChannel } -func (us SqlUserStore) Search(teamId string, term string, searchType string) StoreChannel { +func (us SqlUserStore) Search(teamId string, term string, options map[string]bool) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { searchQuery := "" + if teamId == "" { + + // Id != '' is added because both SEARCH_CLAUSE and INACTIVE_CLAUSE start with an AND searchQuery = ` SELECT * FROM Users WHERE - DeleteAt = 0 + Id != '' SEARCH_CLAUSE + INACTIVE_CLAUSE ORDER BY Username ASC LIMIT 50` } else { @@ -1110,14 +1116,14 @@ func (us SqlUserStore) Search(teamId string, term string, searchType string) Sto WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId - AND Users.DeleteAt = 0 AND TeamMembers.DeleteAt = 0 SEARCH_CLAUSE + INACTIVE_CLAUSE ORDER BY Users.Username ASC LIMIT 100` } - storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId}) + storeChannel <- us.performSearch(searchQuery, term, options, map[string]interface{}{"TeamId": teamId}) close(storeChannel) }() @@ -1125,7 +1131,7 @@ func (us SqlUserStore) Search(teamId string, term string, searchType string) Sto return storeChannel } -func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel { +func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { @@ -1133,34 +1139,38 @@ func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term if teamId == "" { searchQuery = ` SELECT - u.* - FROM Users u + Users.* + FROM Users LEFT JOIN ChannelMembers cm - ON cm.UserId = u.Id + ON cm.UserId = Users.Id AND cm.ChannelId = :ChannelId - WHERE cm.UserId IS NULL - SEARCH_CLAUSE - ORDER BY u.Username ASC + WHERE + cm.UserId IS NULL + SEARCH_CLAUSE + INACTIVE_CLAUSE + ORDER BY Users.Username ASC LIMIT 100` } else { searchQuery = ` SELECT - u.* - FROM Users u + Users.* + FROM Users INNER JOIN TeamMembers tm - ON tm.UserId = u.Id + ON tm.UserId = Users.Id AND tm.TeamId = :TeamId AND tm.DeleteAt = 0 LEFT JOIN ChannelMembers cm - ON cm.UserId = u.Id + ON cm.UserId = Users.Id AND cm.ChannelId = :ChannelId - WHERE cm.UserId IS NULL - SEARCH_CLAUSE - ORDER BY u.Username ASC + WHERE + cm.UserId IS NULL + SEARCH_CLAUSE + INACTIVE_CLAUSE + ORDER BY Users.Username ASC LIMIT 100` } - storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId}) + storeChannel <- us.performSearch(searchQuery, term, options, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId}) close(storeChannel) }() @@ -1168,7 +1178,7 @@ func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term return storeChannel } -func (us SqlUserStore) SearchInChannel(channelId string, term string, searchType string) StoreChannel { +func (us SqlUserStore) SearchInChannel(channelId string, term string, options map[string]bool) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { @@ -1180,12 +1190,12 @@ func (us SqlUserStore) SearchInChannel(channelId string, term string, searchType WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = Users.Id - AND Users.DeleteAt = 0 SEARCH_CLAUSE + INACTIVE_CLAUSE ORDER BY Users.Username ASC LIMIT 100` - storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"ChannelId": channelId}) + storeChannel <- us.performSearch(searchQuery, term, options, map[string]interface{}{"ChannelId": channelId}) close(storeChannel) }() @@ -1193,7 +1203,7 @@ func (us SqlUserStore) SearchInChannel(channelId string, term string, searchType return storeChannel } -func (us SqlUserStore) performSearch(searchQuery string, term string, searchType string, parameters map[string]interface{}) StoreResult { +func (us SqlUserStore) performSearch(searchQuery string, term string, options map[string]bool, parameters map[string]interface{}) StoreResult { result := StoreResult{} // these chars have special meaning and can be treated as spaces @@ -1201,6 +1211,17 @@ func (us SqlUserStore) performSearch(searchQuery string, term string, searchType term = strings.Replace(term, c, " ", -1) } + searchType := USER_SEARCH_TYPE_ALL + if ok := options[USER_SEARCH_OPTION_USERNAME_ONLY]; ok { + searchType = USER_SEARCH_TYPE_USERNAME + } + + if ok := options[USER_SEARCH_OPTION_ALLOW_INACTIVE]; ok { + searchQuery = strings.Replace(searchQuery, "INACTIVE_CLAUSE", "", 1) + } else { + searchQuery = strings.Replace(searchQuery, "INACTIVE_CLAUSE", "AND Users.DeleteAt = 0", 1) + } + if term == "" { searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 23c124cb7..bc7cc69c5 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -942,11 +942,75 @@ func TestUserStoreSearch(t *testing.T) { u2.Email = model.NewId() Must(store.User().Save(u2)) + u3 := &model.User{} + u3.Username = "jimbo" + model.NewId() + u3.Email = model.NewId() + u3.DeleteAt = 1 + Must(store.User().Save(u3)) + tid := model.NewId() Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id})) Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id})) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id})) + + searchOptions := map[string]bool{} + searchOptions[USER_SEARCH_OPTION_USERNAME_ONLY] = true + + if r1 := <-store.User().Search(tid, "jimb", searchOptions); r1.Err != nil { + t.Fatal(r1.Err) + } else { + profiles := r1.Data.([]*model.User) + found1 := false + found2 := false + for _, profile := range profiles { + if profile.Id == u1.Id { + found1 = true + } + + if profile.Id == u3.Id { + found2 = true + } + } + + if !found1 { + t.Fatal("should have found user") + } + + if found2 { + t.Fatal("should not have found inactive user") + } + } - if r1 := <-store.User().Search(tid, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + searchOptions[USER_SEARCH_OPTION_ALLOW_INACTIVE] = true + + if r1 := <-store.User().Search(tid, "jimb", searchOptions); r1.Err != nil { + t.Fatal(r1.Err) + } else { + profiles := r1.Data.([]*model.User) + found1 := false + found2 := false + for _, profile := range profiles { + if profile.Id == u1.Id { + found1 = true + } + + if profile.Id == u3.Id { + found2 = true + } + } + + if !found1 { + t.Fatal("should have found user") + } + + if !found2 { + t.Fatal("should have found inactive user") + } + } + + searchOptions[USER_SEARCH_OPTION_ALLOW_INACTIVE] = false + + if r1 := <-store.User().Search(tid, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -963,7 +1027,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search("", "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().Search("", "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -980,7 +1044,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search("", "jim-bobb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().Search("", "jim-bobb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -998,7 +1062,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search(tid, "", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().Search(tid, "", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } @@ -1009,7 +1073,7 @@ func TestUserStoreSearch(t *testing.T) { c1.Type = model.CHANNEL_OPEN c1 = *Must(store.Channel().Save(&c1)).(*model.Channel) - if r1 := <-store.User().SearchNotInChannel(tid, c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().SearchNotInChannel(tid, c1.Id, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1026,7 +1090,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().SearchNotInChannel("", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().SearchNotInChannel("", c1.Id, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1043,7 +1107,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().SearchNotInChannel("junk", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().SearchNotInChannel("junk", c1.Id, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1060,7 +1124,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1079,7 +1143,7 @@ func TestUserStoreSearch(t *testing.T) { Must(store.Channel().SaveMember(&model.ChannelMember{ChannelId: c1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()})) - if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil { + if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1096,7 +1160,9 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search(tid, "Tim", USER_SEARCH_TYPE_ALL); r1.Err != nil { + searchOptions = map[string]bool{} + + if r1 := <-store.User().Search(tid, "Tim", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1113,7 +1179,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search(tid, "Bill", USER_SEARCH_TYPE_ALL); r1.Err != nil { + if r1 := <-store.User().Search(tid, "Bill", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) @@ -1130,7 +1196,7 @@ func TestUserStoreSearch(t *testing.T) { } } - if r1 := <-store.User().Search(tid, "Rob", USER_SEARCH_TYPE_ALL); r1.Err != nil { + if r1 := <-store.User().Search(tid, "Rob", searchOptions); r1.Err != nil { t.Fatal(r1.Err) } else { profiles := r1.Data.([]*model.User) diff --git a/store/store.go b/store/store.go index 6cf216699..85a1ad398 100644 --- a/store/store.go +++ b/store/store.go @@ -164,9 +164,9 @@ type UserStore interface { GetUnreadCount(userId string) StoreChannel GetUnreadCountForChannel(userId string, channelId string) StoreChannel GetRecentlyActiveUsersForTeam(teamId string) StoreChannel - Search(teamId string, term string, searchType string) StoreChannel - SearchInChannel(channelId string, term string, searchType string) StoreChannel - SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel + Search(teamId string, term string, options map[string]bool) StoreChannel + SearchInChannel(channelId string, term string, options map[string]bool) StoreChannel + SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel } type SessionStore interface { diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx index 1d0886737..3efb242ed 100644 --- a/webapp/components/admin_console/team_users.jsx +++ b/webapp/components/admin_console/team_users.jsx @@ -13,7 +13,7 @@ import UserStore from 'stores/user_store.jsx'; import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx'; import {getTeamStats, getUser} from 'utils/async_client.jsx'; -import Constants from 'utils/constants.jsx'; +import {Constants, UserSearchOptions} from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; import React from 'react'; @@ -145,10 +145,13 @@ export default class UserList extends React.Component { return; } + const options = {}; + options[UserSearchOptions.ALLOW_INACTIVE] = true; + searchUsers( term, this.props.params.team, - {}, + options, (users) => { this.setState({loading: true, search: true, users}); loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete); diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx index 14e02e04b..7f6ca4d32 100644 --- a/webapp/components/channel_invite_modal.jsx +++ b/webapp/components/channel_invite_modal.jsx @@ -105,7 +105,7 @@ export default class ChannelInviteModal extends React.Component { searchUsers( term, TeamStore.getCurrentId(), - {not_in_channel: this.props.channel.id}, + {not_in_channel_id: this.props.channel.id}, (users) => { this.setState({search: true, users}); } diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx index 286a2243a..76ce535ad 100644 --- a/webapp/components/channel_members_modal.jsx +++ b/webapp/components/channel_members_modal.jsx @@ -117,7 +117,7 @@ export default class ChannelMembersModal extends React.Component { searchUsers( term, TeamStore.getCurrentId(), - {in_channel: this.props.channel.id}, + {in_channel_id: this.props.channel.id}, (users) => { this.setState({search: true, users}); } diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index 52e546cd2..7e57261b6 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -37,7 +37,7 @@ export default class MoreDirectChannels extends React.Component { this.loadComplete = this.loadComplete.bind(this); this.state = { - users: UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true), + users: UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true), loadingDMChannel: -1, listType: 'team', loading: false, @@ -111,7 +111,7 @@ export default class MoreDirectChannels extends React.Component { if (this.state.listType === 'any') { users = UserStore.getProfileList(); } else { - users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true); + users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true); } this.setState({ @@ -125,7 +125,7 @@ export default class MoreDirectChannels extends React.Component { if (listType === 'any') { users = UserStore.getProfileList(); } else { - users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true); + users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true); } this.setState({ diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 9ac8540f9..bb3415a7d 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -312,7 +312,7 @@ class UserStoreClass extends EventEmitter { this.saveProfiles(profiles); } - getProfileListInTeam(teamId = TeamStore.getCurrentId(), skipCurrent) { + getProfileListInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) { const userIds = this.profiles_in_team[teamId] || []; const profiles = []; const currentId = this.getCurrentId(); @@ -324,6 +324,10 @@ class UserStoreClass extends EventEmitter { continue; } + if (skipInactive && profile.delete_at > 0) { + continue; + } + if (profile) { profiles.push(profile); } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index beaca3921..352401142 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -181,6 +181,10 @@ export const UserStatuses = { ONLINE: 'online' }; +export const UserSearchOptions = { + ALLOW_INACTIVE: 'allow_inactive' +}; + export const SocketEvents = { POSTED: 'posted', POST_EDITED: 'post_edited', @@ -214,6 +218,7 @@ export const Constants = { ActionTypes, WebrtcActionTypes, UserStatuses, + UserSearchOptions, TutorialSteps, PayloadSources: keyMirror({ -- cgit v1.2.3-1-g7c22