MM-11725: Add specific autocomplete endpoint for search autocomplete (#9337)

Этот коммит содержится в:
Jesús Espino
2018-09-11 22:45:31 +02:00
коммит произвёл GitHub
родитель d585f9d9a3
Коммит e32581aef3
7 изменённых файлов: 197 добавлений и 0 удалений

Просмотреть файл

@@ -22,6 +22,7 @@ func (api *API) InitChannel() {
api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.ApiSessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search", api.ApiSessionRequired(searchChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET")
api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.ApiSessionRequired(getChannelsForTeamForUser)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.ApiSessionRequired(getChannel)).Methods("GET")
@@ -642,6 +643,30 @@ func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Requ
w.Write([]byte(channels.ToJson()))
}
func autocompleteChannelsForTeamForSearch(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
c.SetPermissionError(model.PERMISSION_LIST_TEAM_CHANNELS)
return
}
name := r.URL.Query().Get("name")
channels, err := c.App.AutocompleteChannelsForSearch(c.Params.TeamId, c.Session.UserId, name)
if err != nil {
c.Err = err
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
w.Write([]byte(channels.ToJson()))
}
func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {

Просмотреть файл

@@ -1507,6 +1507,16 @@ func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelLi
}
}
func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
if result := <-a.Srv.Store.Channel().AutocompleteInTeamForSearch(teamId, userId, term, includeDeleted); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.ChannelList), nil
}
}
func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels

Просмотреть файл

@@ -1961,6 +1961,17 @@ func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList
}
}
// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions
func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) {
query := fmt.Sprintf("?name=%v", name)
if r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, ""); err != nil {
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return ChannelListFromJson(r.Body), BuildResponse(r)
}
}
// Post Section
// CreatePost creates a post based on the provided post struct.

Просмотреть файл

@@ -1589,6 +1589,53 @@ func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeD
})
}
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
deleteFilter := "AND DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
}
queryFormat := `
SELECT
C.*
FROM
Channels AS C
JOIN
ChannelMembers AS CM ON CM.ChannelId = C.Id
WHERE
C.TeamId = :TeamId
AND CM.UserId = :UserId
` + deleteFilter + `
%v
LIMIT 50`
var channels model.ChannelList
if likeClause, likeTerm := s.buildLIKEClause(term); likeClause == "" {
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId, "UserId": userId}); err != nil {
result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
} else {
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
// query you would get using an OR of the LIKE and full-text clauses.
fulltextClause, fulltextTerm := s.buildFulltextClause(term)
likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause)
fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause)
query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery)
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "UserId": userId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil {
result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
}
sort.Slice(channels, func(a, b int) bool {
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
})
result.Data = &channels
})
}
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
deleteFilter := "AND DeleteAt = 0"

Просмотреть файл

@@ -162,6 +162,7 @@ type ChannelStore interface {
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
GetMembersForUser(teamId string, userId string) StoreChannel
AutocompleteInTeam(teamId string, term string, includeDeleted bool) StoreChannel
AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) StoreChannel
SearchInTeam(teamId string, term string, includeDeleted bool) StoreChannel
SearchMore(userId string, teamId string, term string) StoreChannel
GetMembersByIds(channelId string, userIds []string) StoreChannel

Просмотреть файл

@@ -48,6 +48,7 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) })
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) })
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) })
t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) })
t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) })
t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) })
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
@@ -2054,6 +2055,92 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
}
}
func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = "zz" + model.NewId() + "b"
o1.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o1, -1))
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&m1))
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = "zz" + model.NewId() + "b"
o2.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o2, -1))
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&m2))
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "ChannelA"
o3.Name = "zz" + model.NewId() + "b"
o3.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o3, -1))
m3 := model.ChannelMember{}
m3.ChannelId = o3.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&m3))
store.Must(ss.Channel().SetDeleteAt(o3.Id, 100, 100))
o4 := model.Channel{}
o4.TeamId = o1.TeamId
o4.DisplayName = "ChannelA"
o4.Name = "zz" + model.NewId() + "b"
o4.Type = model.CHANNEL_PRIVATE
store.Must(ss.Channel().Save(&o4, -1))
m4 := model.ChannelMember{}
m4.ChannelId = o4.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&m4))
o5 := model.Channel{}
o5.TeamId = o1.TeamId
o5.DisplayName = "ChannelC"
o5.Name = "zz" + model.NewId() + "b"
o5.Type = model.CHANNEL_PRIVATE
store.Must(ss.Channel().Save(&o5, -1))
tt := []struct {
name string
term string
includeDeleted bool
expectedMatches int
}{
{"Empty search (list all)", "", false, 3},
{"Narrow search", "ChannelA", false, 2},
{"Wide search", "Cha", false, 3},
{"Wide search with archived channels", "Cha", true, 4},
{"Narrow with archived channels", "ChannelA", true, 3},
{"Search without results", "blarg", true, 0},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
result := <-ss.Channel().AutocompleteInTeamForSearch(o1.TeamId, m1.UserId, "ChannelA", false)
require.Nil(t, result.Err)
channels := result.Data.(*model.ChannelList)
require.Len(t, *channels, 2)
})
}
}
func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()

Просмотреть файл

@@ -61,6 +61,22 @@ func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string, includeDe
return r0
}
// AutocompleteInTeamForSearch provides a mock function with given fields: teamId, userId, term, includeDeleted
func (_m *ChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel {
ret := _m.Called(teamId, userId, term, includeDeleted)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string, string, bool) store.StoreChannel); ok {
r0 = rf(teamId, userId, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
}
}
return r0
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *ChannelStore) ClearAllCustomRoleAssignments() store.StoreChannel {
ret := _m.Called()