[MM-51201/MM-60406/MM-60404] CrossTeam Search posts and files (#28478)
* poc - wip * add search files across teams * eslint * fix existing tests * fix webapp style * fix test * add api doc * change initial state in test * add tests on API * add tests on file info layer * fix file search tags * add rhs reducer test * reset team selected when the RHS is suppressed * change css to reflect UI * fix style * fix doc wording * make getSearchTeam return currentTeamId when value is not set * await is unnecessary * revert boolean check and add test * add comment to getSearchTeam to let dev knows it defaults to currentTeam * remove redundant team check * simplfy test * fix style check --------- Co-authored-by: Caleb Roseland <caleb@calebroseland.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -39,7 +39,8 @@ func (api *API) InitFile() {
|
||||
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods(http.MethodGet)
|
||||
|
||||
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFiles)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Files.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchFilesInAllTeams)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods(http.MethodGet, http.MethodHead)
|
||||
}
|
||||
@@ -736,7 +737,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
|
||||
}
|
||||
|
||||
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -747,6 +748,14 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
searchFiles(c, w, r, c.Params.TeamId)
|
||||
}
|
||||
|
||||
func searchFilesInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
searchFiles(c, w, r, "")
|
||||
}
|
||||
|
||||
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
|
||||
var params model.SearchParameter
|
||||
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if jsonErr != nil {
|
||||
@@ -787,7 +796,7 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, c.Params.TeamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
|
||||
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||
metrics := c.App.Metrics()
|
||||
|
||||
@@ -1169,7 +1169,7 @@ func TestGetPublicFile(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, "should've failed to get file after it is deleted")
|
||||
}
|
||||
|
||||
func TestSearchFiles(t *testing.T) {
|
||||
func TestSearchFilesInTeam(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||
@@ -1318,3 +1318,61 @@ func TestSearchFiles(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestSearchFilesAcrossTeams(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
|
||||
})
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
|
||||
})
|
||||
data, err := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
th.LoginBasic()
|
||||
client := th.Client
|
||||
|
||||
var teams [2]*model.Team
|
||||
var channels [2]*model.Channel
|
||||
for i := 0; i < 2; i++ {
|
||||
teams[i] = th.CreateTeam()
|
||||
channels[i] = th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, teams[i].Id)
|
||||
|
||||
th.LinkUserToTeam(th.BasicUser, teams[i])
|
||||
th.AddUserToChannel(th.BasicUser, channels[i])
|
||||
|
||||
filename := "search for fileInfo"
|
||||
fileInfo, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
th.CreatePostInChannelWithFiles(channels[i], fileInfo)
|
||||
}
|
||||
|
||||
terms := "search"
|
||||
|
||||
// BasicUser should have access to all the files
|
||||
fileInfos, _, err := client.SearchFilesAcrossTeams(context.Background(), terms, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fileInfos.Order, 2, "wrong search")
|
||||
|
||||
// a new user that only belongs to the first team should only get one result
|
||||
newUser := th.CreateUser()
|
||||
th.LinkUserToTeam(newUser, teams[0])
|
||||
th.AddUserToChannel(newUser, channels[0])
|
||||
th.UnlinkUserFromTeam(th.BasicUser, teams[1])
|
||||
|
||||
_, err = th.Client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
_, _, err = th.Client.Login(context.Background(), newUser.Email, newUser.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
fileInfos, _, err = client.SearchFilesAcrossTeams(context.Background(), terms, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fileInfos.Order, 1, "wrong search")
|
||||
require.Equal(t, fileInfos.FileInfos[fileInfos.Order[0]].ChannelId, channels[0].Id, "wrong search")
|
||||
}
|
||||
|
||||
@@ -198,6 +198,11 @@ var searchFileInfoStoreTests = []searchTest{
|
||||
Fn: testFileInfoSearchShowChannelBookmarkFiles,
|
||||
Tags: []string{EnginePostgres, EngineMySQL, EngineElasticSearch},
|
||||
},
|
||||
{
|
||||
Name: "Should search files across teams",
|
||||
Fn: testFileInfoSearchAcrossTeams,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSearchFileInfoStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
|
||||
@@ -1727,3 +1732,73 @@ func testFileInfoSearchShowChannelBookmarkFiles(t *testing.T, th *SearchTestHelp
|
||||
require.Len(t, results.FileInfos, 1)
|
||||
require.Equal(t, "message test@test.com", results.FileInfos[file.Id].Name)
|
||||
}
|
||||
|
||||
func testFileInfoSearchAcrossTeams(t *testing.T, th *SearchTestHelper) {
|
||||
user1, err := th.createUser("user1", "user1", "user1", "user1")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(user1)
|
||||
|
||||
user2, err := th.createUser("user2", "user2", "user2", "user2")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(user2)
|
||||
|
||||
team1, err := th.createTeam("team1", "team1", model.TeamOpen)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteTeam(team1)
|
||||
|
||||
team2, err := th.createTeam("team2", "team2", model.TeamOpen)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteTeam(team2)
|
||||
|
||||
// user1 join both teams, user2 join team1
|
||||
err = th.addUserToTeams(user1, []string{team1.Id, team2.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToTeams(user2, []string{team1.Id})
|
||||
require.NoError(t, err)
|
||||
|
||||
channel1, err := th.createChannel(team1.Id, "channel1", "channel1", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(channel1)
|
||||
|
||||
channel2, err := th.createChannel(team2.Id, "channel2", "channel2", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(channel2)
|
||||
|
||||
// user1 joins all channels, user2 joins channel1
|
||||
err = th.addUserToChannels(user1, []string{channel1.Id, channel2.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(user2, []string{channel1.Id})
|
||||
require.NoError(t, err)
|
||||
|
||||
postInChannel1, err := th.createPost(user1.Id, channel1.Id, "message", "", model.PostTypeDefault, 0, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUserPosts(user1.Id)
|
||||
postInChannel2, err := th.createPost(user1.Id, channel2.Id, "message", "", model.PostTypeDefault, 0, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUserPosts(user1.Id)
|
||||
|
||||
p1, err := th.createFileInfo(user1.Id, postInChannel1.Id, postInChannel1.ChannelId, "channel test filename", "channel contenttest filename", "jpg", "image/jpeg", 0, 0)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUserFileInfos(th.User.Id)
|
||||
p2, err := th.createFileInfo(user1.Id, postInChannel2.Id, postInChannel2.ChannelId, "channel test filename", "channel contenttest filename", "jpg", "image/jpeg", 0, 0)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUserFileInfos(th.User.Id)
|
||||
|
||||
t.Run("user in all teams", func(t *testing.T) {
|
||||
params := &model.SearchParams{Terms: "test"}
|
||||
results, err := th.Store.FileInfo().Search(th.Context, []*model.SearchParams{params}, user1.Id, "", 0, 20)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, results.FileInfos, 2)
|
||||
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
|
||||
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
|
||||
})
|
||||
t.Run("user in team1", func(t *testing.T) {
|
||||
params := &model.SearchParams{Terms: "test"}
|
||||
results, err := th.Store.FileInfo().Search(th.Context, []*model.SearchParams{params}, user2.Id, "", 0, 20)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, results.FileInfos, 1)
|
||||
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -519,7 +519,6 @@ func (fs SqlFileInfoStore) Search(rctx request.CTX, paramsList []*model.SearchPa
|
||||
From("FileInfo").
|
||||
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
|
||||
LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId").
|
||||
Where(sq.Or{sq.Eq{"C.TeamId": teamId}, sq.Eq{"C.TeamId": ""}}).
|
||||
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"FileInfo.CreatorId": model.BookmarkFileOwner},
|
||||
@@ -528,6 +527,10 @@ func (fs SqlFileInfoStore) Search(rctx request.CTX, paramsList []*model.SearchPa
|
||||
OrderBy("FileInfo.CreateAt DESC").
|
||||
Limit(100)
|
||||
|
||||
if teamId != "" {
|
||||
query = query.Where(sq.Or{sq.Eq{"C.TeamId": teamId}, sq.Eq{"C.TeamId": ""}})
|
||||
}
|
||||
|
||||
for _, params := range paramsList {
|
||||
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user