[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>
Этот коммит содержится в:
@@ -375,7 +375,7 @@
|
|||||||
from a user include `from:someusername`, using a user's
|
from a user include `from:someusername`, using a user's
|
||||||
username. To search in a specific channel include
|
username. To search in a specific channel include
|
||||||
`in:somechannel`, using the channel name (not the display
|
`in:somechannel`, using the channel name (not the display
|
||||||
name). To search for specific extensions included `ext:extension`.
|
name). To search for specific extensions include `ext:extension`.
|
||||||
is_or_search:
|
is_or_search:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Set to true if an Or search should be performed vs an And
|
description: Set to true if an Or search should be performed vs an And
|
||||||
@@ -411,3 +411,72 @@
|
|||||||
$ref: "#/components/responses/Unauthorized"
|
$ref: "#/components/responses/Unauthorized"
|
||||||
"403":
|
"403":
|
||||||
$ref: "#/components/responses/Forbidden"
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"/api/v4/files/search":
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- files
|
||||||
|
- search
|
||||||
|
summary: Search files across the teams of the current user
|
||||||
|
description: >
|
||||||
|
Search for files in the teams of the current user based on file name,
|
||||||
|
extention and file content (if file content extraction is enabled and
|
||||||
|
supported for the files).
|
||||||
|
|
||||||
|
__Minimum server version__: 10.2
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
|
||||||
|
Must be authenticated and have the `view_team` permission.
|
||||||
|
operationId: SearchFiles
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- terms
|
||||||
|
- is_or_search
|
||||||
|
properties:
|
||||||
|
terms:
|
||||||
|
type: string
|
||||||
|
description: The search terms as entered by the user. To search for files
|
||||||
|
from a user include `from:someusername`, using a user's
|
||||||
|
username. To search in a specific channel include
|
||||||
|
`in:somechannel`, using the channel name (not the display
|
||||||
|
name). To search for specific extensions include `ext:extension`.
|
||||||
|
is_or_search:
|
||||||
|
type: boolean
|
||||||
|
description: Set to true if an Or search should be performed vs an And
|
||||||
|
search.
|
||||||
|
time_zone_offset:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
description: Offset from UTC of user timezone for date searches.
|
||||||
|
include_deleted_channels:
|
||||||
|
type: boolean
|
||||||
|
description: Set to true if deleted channels should be included in the
|
||||||
|
search. (archived channels)
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
description: The page to select. (Only works with Elasticsearch)
|
||||||
|
per_page:
|
||||||
|
type: integer
|
||||||
|
default: 60
|
||||||
|
description: The number of posts per page. (Only works with Elasticsearch)
|
||||||
|
description: The search terms and logic to use in the search.
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Files list retrieval successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FileInfoList"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ func (api *API) InitFile() {
|
|||||||
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods(http.MethodGet)
|
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods(http.MethodGet)
|
||||||
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).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)
|
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)
|
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()
|
c.RequireTeamId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
return
|
return
|
||||||
@@ -747,6 +748,14 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
var params model.SearchParameter
|
||||||
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
||||||
if jsonErr != nil {
|
if jsonErr != nil {
|
||||||
@@ -787,7 +796,7 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
startTime := time.Now()
|
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)
|
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||||
metrics := c.App.Metrics()
|
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")
|
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()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
|
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||||
@@ -1318,3 +1318,61 @@ func TestSearchFiles(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
CheckUnauthorizedStatus(t, resp)
|
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,
|
Fn: testFileInfoSearchShowChannelBookmarkFiles,
|
||||||
Tags: []string{EnginePostgres, EngineMySQL, EngineElasticSearch},
|
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) {
|
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.Len(t, results.FileInfos, 1)
|
||||||
require.Equal(t, "message test@test.com", results.FileInfos[file.Id].Name)
|
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").
|
From("FileInfo").
|
||||||
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
|
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
|
||||||
LeftJoin("ChannelMembers as CM ON C.Id=CM.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.Eq{"FileInfo.DeleteAt": 0}).
|
||||||
Where(sq.Or{
|
Where(sq.Or{
|
||||||
sq.Eq{"FileInfo.CreatorId": model.BookmarkFileOwner},
|
sq.Eq{"FileInfo.CreatorId": model.BookmarkFileOwner},
|
||||||
@@ -528,6 +527,10 @@ func (fs SqlFileInfoStore) Search(rctx request.CTX, paramsList []*model.SearchPa
|
|||||||
OrderBy("FileInfo.CreateAt DESC").
|
OrderBy("FileInfo.CreateAt DESC").
|
||||||
Limit(100)
|
Limit(100)
|
||||||
|
|
||||||
|
if teamId != "" {
|
||||||
|
query = query.Where(sq.Or{sq.Eq{"C.TeamId": teamId}, sq.Eq{"C.TeamId": ""}})
|
||||||
|
}
|
||||||
|
|
||||||
for _, params := range paramsList {
|
for _, params := range paramsList {
|
||||||
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
|
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
|
||||||
|
|
||||||
|
|||||||
@@ -4415,7 +4415,12 @@ func (c *Client4) SearchFilesWithParams(ctx context.Context, teamId string, para
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
r, err := c.DoAPIPost(ctx, c.teamRoute(teamId)+"/files/search", string(js))
|
|
||||||
|
route := c.teamRoute(teamId) + "/files/search"
|
||||||
|
if teamId == "" {
|
||||||
|
route = c.filesRoute() + "/search"
|
||||||
|
}
|
||||||
|
r, err := c.DoAPIPost(ctx, route, string(js))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, BuildResponse(r), err
|
return nil, BuildResponse(r), err
|
||||||
}
|
}
|
||||||
@@ -4428,6 +4433,15 @@ func (c *Client4) SearchFilesWithParams(ctx context.Context, teamId string, para
|
|||||||
return &list, BuildResponse(r), nil
|
return &list, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchFilesAcrossTeams returns any posts with matching terms string.
|
||||||
|
func (c *Client4) SearchFilesAcrossTeams(ctx context.Context, terms string, isOrSearch bool) (*FileInfoList, *Response, error) {
|
||||||
|
params := SearchParameter{
|
||||||
|
Terms: &terms,
|
||||||
|
IsOrSearch: &isOrSearch,
|
||||||
|
}
|
||||||
|
return c.SearchFilesWithParams(ctx, "", ¶ms)
|
||||||
|
}
|
||||||
|
|
||||||
// SearchPosts returns any posts with matching terms string.
|
// SearchPosts returns any posts with matching terms string.
|
||||||
func (c *Client4) SearchPosts(ctx context.Context, teamId string, terms string, isOrSearch bool) (*PostList, *Response, error) {
|
func (c *Client4) SearchPosts(ctx context.Context, teamId string, terms string, isOrSearch bool) (*PostList, *Response, error) {
|
||||||
params := SearchParameter{
|
params := SearchParameter{
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ type FeatureFlags struct {
|
|||||||
NotificationMonitoring bool
|
NotificationMonitoring bool
|
||||||
|
|
||||||
ExperimentalAuditSettingsSystemConsoleUI bool
|
ExperimentalAuditSettingsSystemConsoleUI bool
|
||||||
|
|
||||||
|
ExperimentalCrossTeamSearch bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FeatureFlags) SetDefaults() {
|
func (f *FeatureFlags) SetDefaults() {
|
||||||
@@ -78,6 +80,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.WebSocketEventScope = true
|
f.WebSocketEventScope = true
|
||||||
f.NotificationMonitoring = true
|
f.NotificationMonitoring = true
|
||||||
f.ExperimentalAuditSettingsSystemConsoleUI = false
|
f.ExperimentalAuditSettingsSystemConsoleUI = false
|
||||||
|
f.ExperimentalCrossTeamSearch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap returns the feature flags as a map[string]string
|
// ToMap returns the feature flags as a map[string]string
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
goBack,
|
goBack,
|
||||||
showChannelMembers,
|
showChannelMembers,
|
||||||
openShowEditHistory,
|
openShowEditHistory,
|
||||||
|
updateSearchTeam,
|
||||||
} from 'actions/views/rhs';
|
} from 'actions/views/rhs';
|
||||||
|
|
||||||
import mockStore from 'tests/test_store';
|
import mockStore from 'tests/test_store';
|
||||||
@@ -229,7 +230,7 @@ describe('rhs view actions', () => {
|
|||||||
|
|
||||||
test('it dispatches searchPosts correctly', () => {
|
test('it dispatches searchPosts correctly', () => {
|
||||||
const terms = '@here test search';
|
const terms = '@here test search';
|
||||||
store.dispatch(performSearch(terms, false));
|
store.dispatch(performSearch(terms, currentTeamId, false));
|
||||||
|
|
||||||
const compareStore = mockStore(initialState);
|
const compareStore = mockStore(initialState);
|
||||||
compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
||||||
@@ -251,7 +252,7 @@ describe('rhs view actions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const terms = '@here test search';
|
const terms = '@here test search';
|
||||||
store.dispatch(performSearch(terms, false));
|
store.dispatch(performSearch(terms, currentTeamId, false));
|
||||||
|
|
||||||
const filesExtTerms = '@here test search ext:txt ext:jpeg';
|
const filesExtTerms = '@here test search ext:txt ext:jpeg';
|
||||||
const compareStore = mockStore(initialState);
|
const compareStore = mockStore(initialState);
|
||||||
@@ -263,12 +264,12 @@ describe('rhs view actions', () => {
|
|||||||
|
|
||||||
test('it dispatches searchPosts correctly for Recent Mentions', () => {
|
test('it dispatches searchPosts correctly for Recent Mentions', () => {
|
||||||
const terms = `@here test search ${currentUsername} @${currentUsername} ${currentUserFirstName}`;
|
const terms = `@here test search ${currentUsername} @${currentUsername} ${currentUserFirstName}`;
|
||||||
store.dispatch(performSearch(terms, true));
|
store.dispatch(performSearch(terms, '', true));
|
||||||
|
|
||||||
const mentionsQuotedTerms = `@here test search "${currentUsername}" "@${currentUsername}" "${currentUserFirstName}"`;
|
const mentionsQuotedTerms = `@here test search "${currentUsername}" "@${currentUsername}" "${currentUserFirstName}"`;
|
||||||
const compareStore = mockStore(initialState);
|
const compareStore = mockStore(initialState);
|
||||||
compareStore.dispatch(SearchActions.searchPostsWithParams('', {include_deleted_channels: false, terms: mentionsQuotedTerms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
compareStore.dispatch(SearchActions.searchPostsWithParams('', {include_deleted_channels: false, terms: mentionsQuotedTerms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
||||||
compareStore.dispatch(SearchActions.searchFilesWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
compareStore.dispatch(SearchActions.searchFilesWithParams('', {include_deleted_channels: false, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
|
||||||
|
|
||||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||||
});
|
});
|
||||||
@@ -282,6 +283,7 @@ describe('rhs view actions', () => {
|
|||||||
views: {
|
views: {
|
||||||
rhs: {
|
rhs: {
|
||||||
searchTerms: terms,
|
searchTerms: terms,
|
||||||
|
searchTeam: null,
|
||||||
searchType: 'messages',
|
searchType: 'messages',
|
||||||
filesSearchExtFilter: [] as string[],
|
filesSearchExtFilter: [] as string[],
|
||||||
},
|
},
|
||||||
@@ -303,7 +305,7 @@ describe('rhs view actions', () => {
|
|||||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||||
searchType: 'messages',
|
searchType: 'messages',
|
||||||
});
|
});
|
||||||
compareStore.dispatch(performSearch(terms));
|
compareStore.dispatch(performSearch(terms, currentTeamId));
|
||||||
|
|
||||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||||
});
|
});
|
||||||
@@ -483,7 +485,7 @@ describe('rhs view actions', () => {
|
|||||||
|
|
||||||
const compareStore = mockStore(initialState);
|
const compareStore = mockStore(initialState);
|
||||||
|
|
||||||
compareStore.dispatch(performSearch('@mattermost ', true));
|
compareStore.dispatch(performSearch('@mattermost ', '', true));
|
||||||
compareStore.dispatch(batchActions([
|
compareStore.dispatch(batchActions([
|
||||||
{
|
{
|
||||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||||
@@ -740,6 +742,7 @@ describe('rhs view actions', () => {
|
|||||||
|
|
||||||
compareStore.dispatch(SearchActions.clearSearch());
|
compareStore.dispatch(SearchActions.clearSearch());
|
||||||
compareStore.dispatch(updateSearchTerms(''));
|
compareStore.dispatch(updateSearchTerms(''));
|
||||||
|
compareStore.dispatch(updateSearchTeam(null));
|
||||||
compareStore.dispatch({
|
compareStore.dispatch({
|
||||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS,
|
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS,
|
||||||
terms: '',
|
terms: '',
|
||||||
@@ -759,7 +762,7 @@ describe('rhs view actions', () => {
|
|||||||
store.dispatch(openAtPrevious({isMentionSearch: true}));
|
store.dispatch(openAtPrevious({isMentionSearch: true}));
|
||||||
const compareStore = mockStore(initialState);
|
const compareStore = mockStore(initialState);
|
||||||
|
|
||||||
compareStore.dispatch(performSearch('@mattermost ', true));
|
compareStore.dispatch(performSearch('@mattermost ', '', true));
|
||||||
compareStore.dispatch(batchActions([
|
compareStore.dispatch(batchActions([
|
||||||
{
|
{
|
||||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||||
@@ -872,6 +875,7 @@ describe('rhs view actions', () => {
|
|||||||
views: {
|
views: {
|
||||||
rhs: {
|
rhs: {
|
||||||
searchTerms: terms,
|
searchTerms: terms,
|
||||||
|
searchTeam: null,
|
||||||
searchType: 'messages',
|
searchType: 'messages',
|
||||||
filesSearchExtFilter: [] as string[],
|
filesSearchExtFilter: [] as string[],
|
||||||
},
|
},
|
||||||
@@ -891,7 +895,8 @@ describe('rhs view actions', () => {
|
|||||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||||
searchType: 'messages',
|
searchType: 'messages',
|
||||||
});
|
});
|
||||||
compareStore.dispatch(performSearch(terms));
|
|
||||||
|
compareStore.dispatch(performSearch(terms, currentTeamId));
|
||||||
|
|
||||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
getPluggableId,
|
getPluggableId,
|
||||||
getFilesSearchExtFilter,
|
getFilesSearchExtFilter,
|
||||||
getPreviousRhsState,
|
getPreviousRhsState,
|
||||||
|
getSearchTeam,
|
||||||
} from 'selectors/rhs';
|
} from 'selectors/rhs';
|
||||||
|
|
||||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
@@ -144,6 +145,13 @@ export function updateSearchTerms(terms: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateSearchTeam(teamId: string | null) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.UPDATE_RHS_SEARCH_TEAM,
|
||||||
|
teamId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function setRhsSize(rhsSize?: SidebarSize) {
|
export function setRhsSize(rhsSize?: SidebarSize) {
|
||||||
let newSidebarSize = rhsSize;
|
let newSidebarSize = rhsSize;
|
||||||
if (!newSidebarSize) {
|
if (!newSidebarSize) {
|
||||||
@@ -201,10 +209,9 @@ function updateSearchResultsType(searchType: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function performSearch(terms: string, isMentionSearch?: boolean): ThunkActionFunc<unknown, GlobalState> {
|
export function performSearch(terms: string, teamId: string, isMentionSearch?: boolean): ThunkActionFunc<unknown, GlobalState> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let searchTerms = terms;
|
let searchTerms = terms;
|
||||||
const teamId = getCurrentTeamId(getState());
|
|
||||||
const config = getConfig(getState());
|
const config = getConfig(getState());
|
||||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||||
const extensionsFilters = getFilesSearchExtFilter(getState());
|
const extensionsFilters = getFilesSearchExtFilter(getState());
|
||||||
@@ -236,7 +243,7 @@ export function performSearch(terms: string, isMentionSearch?: boolean): ThunkAc
|
|||||||
// timezone offset in seconds
|
// timezone offset in seconds
|
||||||
const userCurrentTimezone = getCurrentTimezone(getState());
|
const userCurrentTimezone = getCurrentTimezone(getState());
|
||||||
const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
|
const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
|
||||||
const messagesPromise = dispatch(searchPostsWithParams(isMentionSearch ? '' : teamId, {terms: searchTerms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
const messagesPromise = dispatch(searchPostsWithParams(teamId, {terms: searchTerms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||||
const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||||
return Promise.all([filesPromise, messagesPromise]);
|
return Promise.all([filesPromise, messagesPromise]);
|
||||||
};
|
};
|
||||||
@@ -254,17 +261,19 @@ export function showSearchResults(isMentionSearch = false): ThunkActionFunc<unkn
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const searchTerms = getSearchTerms(state);
|
const searchTerms = getSearchTerms(state);
|
||||||
|
let teamId = getSearchTeam(state);
|
||||||
const searchType = getSearchType(state);
|
const searchType = getSearchType(state);
|
||||||
|
|
||||||
if (isMentionSearch) {
|
if (isMentionSearch) {
|
||||||
dispatch(updateRhsState(RHSStates.MENTION));
|
dispatch(updateRhsState(RHSStates.MENTION));
|
||||||
|
teamId = '';
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateRhsState(RHSStates.SEARCH));
|
dispatch(updateRhsState(RHSStates.SEARCH));
|
||||||
}
|
}
|
||||||
dispatch(updateSearchResultsTerms(searchTerms));
|
dispatch(updateSearchResultsTerms(searchTerms));
|
||||||
dispatch(updateSearchResultsType(searchType));
|
dispatch(updateSearchResultsType(searchType));
|
||||||
|
|
||||||
return dispatch(performSearch(searchTerms));
|
return dispatch(performSearch(searchTerms, teamId));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +415,7 @@ export function showPinnedPosts(channelId?: string): ActionFuncAsync<boolean, Gl
|
|||||||
export function showChannelFiles(channelId: string): ActionFuncAsync<boolean, GlobalState> {
|
export function showChannelFiles(channelId: string): ActionFuncAsync<boolean, GlobalState> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const teamId = getCurrentTeamId(state);
|
const teamId = getSearchTeam(state);
|
||||||
|
|
||||||
let previousRhsState = getRhsState(state);
|
let previousRhsState = getRhsState(state);
|
||||||
if (previousRhsState === RHSStates.CHANNEL_FILES) {
|
if (previousRhsState === RHSStates.CHANNEL_FILES) {
|
||||||
@@ -420,7 +429,7 @@ export function showChannelFiles(channelId: string): ActionFuncAsync<boolean, Gl
|
|||||||
});
|
});
|
||||||
dispatch(updateSearchType('files'));
|
dispatch(updateSearchType('files'));
|
||||||
|
|
||||||
const results = await dispatch(performSearch('channel:' + channelId));
|
const results = await dispatch(performSearch('channel:' + channelId, teamId));
|
||||||
const fileData = results instanceof Array ? results[0].data : null;
|
const fileData = results instanceof Array ? results[0].data : null;
|
||||||
const missingPostIds: string[] = [];
|
const missingPostIds: string[] = [];
|
||||||
|
|
||||||
@@ -471,7 +480,7 @@ export function showMentions(): ActionFunc<boolean, GlobalState> {
|
|||||||
|
|
||||||
trackEvent('api', 'api_posts_search_mention');
|
trackEvent('api', 'api_posts_search_mention');
|
||||||
|
|
||||||
dispatch(performSearch(terms, true));
|
dispatch(performSearch(terms, '', true));
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
{
|
{
|
||||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||||
@@ -604,6 +613,7 @@ export function openRHSSearch(): ActionFunc {
|
|||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(clearSearch());
|
dispatch(clearSearch());
|
||||||
dispatch(updateSearchTerms(''));
|
dispatch(updateSearchTerms(''));
|
||||||
|
dispatch(updateSearchTeam(null));
|
||||||
dispatch(updateSearchResultsTerms(''));
|
dispatch(updateSearchResultsTerms(''));
|
||||||
|
|
||||||
dispatch(updateRhsState(RHSStates.SEARCH));
|
dispatch(updateRhsState(RHSStates.SEARCH));
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import type {Dispatch} from 'redux';
|
|||||||
|
|
||||||
import {getMorePostsForSearch, getMoreFilesForSearch} from 'mattermost-redux/actions/search';
|
import {getMorePostsForSearch, getMoreFilesForSearch} from 'mattermost-redux/actions/search';
|
||||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
import {autocompleteChannelsForSearch} from 'actions/channel_actions';
|
import {autocompleteChannelsForSearch} from 'actions/channel_actions';
|
||||||
import {autocompleteUsersInTeam} from 'actions/user_actions';
|
import {autocompleteUsersInTeam} from 'actions/user_actions';
|
||||||
import {
|
import {
|
||||||
updateSearchTerms,
|
updateSearchTerms,
|
||||||
|
updateSearchTeam,
|
||||||
updateSearchTermsForShortcut,
|
updateSearchTermsForShortcut,
|
||||||
showSearchResults,
|
showSearchResults,
|
||||||
showChannelFiles,
|
showChannelFiles,
|
||||||
@@ -24,7 +26,7 @@ import {
|
|||||||
filterFilesSearchByExt,
|
filterFilesSearchByExt,
|
||||||
updateSearchType,
|
updateSearchType,
|
||||||
} from 'actions/views/rhs';
|
} from 'actions/views/rhs';
|
||||||
import {getRhsState, getSearchTerms, getSearchType, getIsSearchingTerm, getIsRhsOpen, getIsRhsExpanded} from 'selectors/rhs';
|
import {getRhsState, getSearchTeam, getSearchTerms, getSearchType, getIsSearchingTerm, getIsRhsOpen, getIsRhsExpanded} from 'selectors/rhs';
|
||||||
import {getIsMobileView} from 'selectors/views/browser';
|
import {getIsMobileView} from 'selectors/views/browser';
|
||||||
|
|
||||||
import {RHSStates} from 'utils/constants';
|
import {RHSStates} from 'utils/constants';
|
||||||
@@ -39,12 +41,18 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
const isMobileView = getIsMobileView(state);
|
const isMobileView = getIsMobileView(state);
|
||||||
const isRhsOpen = getIsRhsOpen(state);
|
const isRhsOpen = getIsRhsOpen(state);
|
||||||
|
|
||||||
|
let searchTeam = getSearchTeam(state);
|
||||||
|
if (!searchTeam) {
|
||||||
|
searchTeam = currentChannel?.team_id || '';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentChannel,
|
currentChannel,
|
||||||
isRhsExpanded: getIsRhsExpanded(state),
|
isRhsExpanded: getIsRhsExpanded(state),
|
||||||
isRhsOpen,
|
isRhsOpen,
|
||||||
isSearchingTerm: getIsSearchingTerm(state),
|
isSearchingTerm: getIsSearchingTerm(state),
|
||||||
searchTerms: getSearchTerms(state),
|
searchTerms: getSearchTerms(state),
|
||||||
|
searchTeam,
|
||||||
searchType: getSearchType(state),
|
searchType: getSearchType(state),
|
||||||
searchVisible: rhsState !== null && (![
|
searchVisible: rhsState !== null && (![
|
||||||
RHSStates.PLUGIN,
|
RHSStates.PLUGIN,
|
||||||
@@ -58,6 +66,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
isPinnedPosts: rhsState === RHSStates.PIN,
|
isPinnedPosts: rhsState === RHSStates.PIN,
|
||||||
isChannelFiles: rhsState === RHSStates.CHANNEL_FILES,
|
isChannelFiles: rhsState === RHSStates.CHANNEL_FILES,
|
||||||
isMobileView,
|
isMobileView,
|
||||||
|
crossTeamSearchEnabled: getFeatureFlagValue(state, 'ExperimentalCrossTeamSearch') === 'true',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +74,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
return {
|
return {
|
||||||
actions: bindActionCreators({
|
actions: bindActionCreators({
|
||||||
updateSearchTerms,
|
updateSearchTerms,
|
||||||
|
updateSearchTeam,
|
||||||
updateSearchTermsForShortcut,
|
updateSearchTermsForShortcut,
|
||||||
updateSearchType,
|
updateSearchType,
|
||||||
showSearchResults,
|
showSearchResults,
|
||||||
|
|||||||
@@ -207,6 +207,14 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
handleUpdateSearchTerms(pretextArray.join(' '));
|
handleUpdateSearchTerms(pretextArray.join(' '));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateSearchTeam = async (teamId: string) => {
|
||||||
|
actions.updateSearchTeam(teamId);
|
||||||
|
handleSearch().then(() => {
|
||||||
|
setKeepInputFocused(false);
|
||||||
|
setFocused(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateSearchTerms = (terms: string): void => {
|
const handleUpdateSearchTerms = (terms: string): void => {
|
||||||
actions.updateSearchTerms(terms);
|
actions.updateSearchTerms(terms);
|
||||||
updateHighlightedSearchHint();
|
updateHighlightedSearchHint();
|
||||||
@@ -310,6 +318,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
actions.updateRhsState(RHSStates.SEARCH);
|
actions.updateRhsState(RHSStates.SEARCH);
|
||||||
}
|
}
|
||||||
actions.updateSearchTerms('');
|
actions.updateSearchTerms('');
|
||||||
|
actions.updateSearchTeam(null);
|
||||||
actions.updateSearchType('');
|
actions.updateSearchType('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -544,6 +553,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
channelDisplayName={props.channelDisplayName}
|
channelDisplayName={props.channelDisplayName}
|
||||||
isOpened={props.isSideBarRightOpen}
|
isOpened={props.isSideBarRightOpen}
|
||||||
updateSearchTerms={handleAddSearchTerm}
|
updateSearchTerms={handleAddSearchTerm}
|
||||||
|
updateSearchTeam={handleUpdateSearchTeam}
|
||||||
handleSearchHintSelection={handleSearchHintSelection}
|
handleSearchHintSelection={handleSearchHintSelection}
|
||||||
isSideBarExpanded={props.isRhsExpanded}
|
isSideBarExpanded={props.isRhsExpanded}
|
||||||
getMorePostsForSearch={props.actions.getMorePostsForSearch}
|
getMorePostsForSearch={props.actions.getMorePostsForSearch}
|
||||||
@@ -552,6 +562,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
searchFilterType={searchFilterType}
|
searchFilterType={searchFilterType}
|
||||||
setSearchType={(value: SearchType) => actions.updateSearchType(value)}
|
setSearchType={(value: SearchType) => actions.updateSearchType(value)}
|
||||||
searchType={searchType || 'messages'}
|
searchType={searchType || 'messages'}
|
||||||
|
crossTeamSearchEnabled={props.crossTeamSearchEnabled}
|
||||||
/>
|
/>
|
||||||
) : props.children}
|
) : props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type StateProps = {
|
|||||||
isRhsOpen: boolean;
|
isRhsOpen: boolean;
|
||||||
isSearchingTerm: boolean;
|
isSearchingTerm: boolean;
|
||||||
searchTerms: string;
|
searchTerms: string;
|
||||||
|
searchTeam: string;
|
||||||
searchType: SearchType;
|
searchType: SearchType;
|
||||||
searchVisible: boolean;
|
searchVisible: boolean;
|
||||||
hideMobileSearchBarInRHS: boolean;
|
hideMobileSearchBarInRHS: boolean;
|
||||||
@@ -36,11 +37,13 @@ export type StateProps = {
|
|||||||
isChannelFiles: boolean;
|
isChannelFiles: boolean;
|
||||||
currentChannel?: Channel;
|
currentChannel?: Channel;
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
|
crossTeamSearchEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DispatchProps = {
|
export type DispatchProps = {
|
||||||
actions: {
|
actions: {
|
||||||
updateSearchTerms: (term: string) => Action;
|
updateSearchTerms: (term: string) => Action;
|
||||||
|
updateSearchTeam: (teamId: string|null) => Action;
|
||||||
updateSearchTermsForShortcut: () => void;
|
updateSearchTermsForShortcut: () => void;
|
||||||
updateSearchType: (searchType: string) => Action;
|
updateSearchType: (searchType: string) => Action;
|
||||||
showSearchResults: (isMentionSearch: boolean) => unknown;
|
showSearchResults: (isMentionSearch: boolean) => unknown;
|
||||||
|
|||||||
@@ -1,117 +1,139 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on files selected 1`] = `
|
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on files selected 1`] = `
|
||||||
<div
|
<ContextProvider
|
||||||
className="MessagesOrFilesSelector"
|
value={
|
||||||
|
Object {
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"subscription": Subscription {
|
||||||
|
"handleChangeWrapper": [Function],
|
||||||
|
"listeners": Object {
|
||||||
|
"notify": [Function],
|
||||||
|
},
|
||||||
|
"onStateChange": [Function],
|
||||||
|
"parentSub": undefined,
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"unsubscribe": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<MessagesOrFilesSelector
|
||||||
className="buttons-container"
|
crossTeamSearchEnabled={false}
|
||||||
>
|
filesCounter="10"
|
||||||
<button
|
isFileAttachmentsEnabled={true}
|
||||||
className="tab messages-tab"
|
messagesCounter="5"
|
||||||
onClick={[Function]}
|
onChange={[MockFunction]}
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Messages"
|
|
||||||
id="search_bar.messages_tab"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="counter"
|
|
||||||
>
|
|
||||||
5
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="active tab files-tab"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Files"
|
|
||||||
id="search_bar.files_tab"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="counter"
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<FilesFilterMenu
|
|
||||||
onFilter={[MockFunction]}
|
onFilter={[MockFunction]}
|
||||||
|
onTeamChange={[MockFunction]}
|
||||||
|
selected="files"
|
||||||
selectedFilter="code"
|
selectedFilter="code"
|
||||||
/>
|
/>
|
||||||
</div>
|
</ContextProvider>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on messages selected 1`] = `
|
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on messages selected 1`] = `
|
||||||
<div
|
<ContextProvider
|
||||||
className="MessagesOrFilesSelector"
|
value={
|
||||||
|
Object {
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"subscription": Subscription {
|
||||||
|
"handleChangeWrapper": [Function],
|
||||||
|
"listeners": Object {
|
||||||
|
"notify": [Function],
|
||||||
|
},
|
||||||
|
"onStateChange": [Function],
|
||||||
|
"parentSub": undefined,
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"unsubscribe": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<MessagesOrFilesSelector
|
||||||
className="buttons-container"
|
crossTeamSearchEnabled={false}
|
||||||
>
|
filesCounter="10"
|
||||||
<button
|
isFileAttachmentsEnabled={true}
|
||||||
className="active tab messages-tab"
|
messagesCounter="5"
|
||||||
onClick={[Function]}
|
onChange={[MockFunction]}
|
||||||
onKeyDown={[Function]}
|
onFilter={[MockFunction]}
|
||||||
>
|
onTeamChange={[MockFunction]}
|
||||||
<MemoizedFormattedMessage
|
selected="messages"
|
||||||
defaultMessage="Messages"
|
selectedFilter="code"
|
||||||
id="search_bar.messages_tab"
|
/>
|
||||||
/>
|
</ContextProvider>
|
||||||
<span
|
|
||||||
className="counter"
|
|
||||||
>
|
|
||||||
5
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="tab files-tab"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Files"
|
|
||||||
id="search_bar.files_tab"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="counter"
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, without files tab 1`] = `
|
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, without files tab 1`] = `
|
||||||
<div
|
<ContextProvider
|
||||||
className="MessagesOrFilesSelector"
|
value={
|
||||||
|
Object {
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"subscription": Subscription {
|
||||||
|
"handleChangeWrapper": [Function],
|
||||||
|
"listeners": Object {
|
||||||
|
"notify": [Function],
|
||||||
|
},
|
||||||
|
"onStateChange": [Function],
|
||||||
|
"parentSub": undefined,
|
||||||
|
"store": Object {
|
||||||
|
"clearActions": [Function],
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getActions": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
},
|
||||||
|
"unsubscribe": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<MessagesOrFilesSelector
|
||||||
className="buttons-container"
|
crossTeamSearchEnabled={false}
|
||||||
>
|
filesCounter="10"
|
||||||
<button
|
isFileAttachmentsEnabled={false}
|
||||||
className="tab messages-tab"
|
messagesCounter="5"
|
||||||
onClick={[Function]}
|
onChange={[MockFunction]}
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Messages"
|
|
||||||
id="search_bar.messages_tab"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="counter"
|
|
||||||
>
|
|
||||||
5
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<FilesFilterMenu
|
|
||||||
onFilter={[MockFunction]}
|
onFilter={[MockFunction]}
|
||||||
|
onTeamChange={[MockFunction]}
|
||||||
|
selected="files"
|
||||||
selectedFilter="code"
|
selectedFilter="code"
|
||||||
/>
|
/>
|
||||||
</div>
|
</ContextProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
.MessagesOrFilesSelector {
|
.MessagesOrFilesSelector {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
|
||||||
.buttons-container {
|
.buttons-container {
|
||||||
|
flex-grow: 1;
|
||||||
margin: 7px 0;
|
margin: 7px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,30 @@
|
|||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
import type {ShallowWrapper} from 'enzyme';
|
import type {ShallowWrapper} from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {Provider} from 'react-redux';
|
||||||
|
|
||||||
import MessagesOrFilesSelector from 'components/search_results/messages_or_files_selector';
|
import MessagesOrFilesSelector from 'components/search_results/messages_or_files_selector';
|
||||||
|
|
||||||
|
import mockStore from 'tests/test_store';
|
||||||
|
|
||||||
describe('components/search_results/MessagesOrFilesSelector', () => {
|
describe('components/search_results/MessagesOrFilesSelector', () => {
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
test('should match snapshot, on messages selected', () => {
|
test('should match snapshot, on messages selected', () => {
|
||||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||||
<MessagesOrFilesSelector
|
<Provider store={store}>
|
||||||
selected='messages'
|
<MessagesOrFilesSelector
|
||||||
selectedFilter='code'
|
selected='messages'
|
||||||
messagesCounter='5'
|
selectedFilter='code'
|
||||||
filesCounter='10'
|
messagesCounter='5'
|
||||||
isFileAttachmentsEnabled={true}
|
filesCounter='10'
|
||||||
onChange={jest.fn()}
|
isFileAttachmentsEnabled={true}
|
||||||
onFilter={jest.fn()}
|
onChange={jest.fn()}
|
||||||
/>,
|
onFilter={jest.fn()}
|
||||||
|
onTeamChange={jest.fn()}
|
||||||
|
crossTeamSearchEnabled={false}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@@ -26,30 +35,41 @@ describe('components/search_results/MessagesOrFilesSelector', () => {
|
|||||||
|
|
||||||
test('should match snapshot, on files selected', () => {
|
test('should match snapshot, on files selected', () => {
|
||||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||||
<MessagesOrFilesSelector
|
|
||||||
selected='files'
|
<Provider store={store}>
|
||||||
selectedFilter='code'
|
<MessagesOrFilesSelector
|
||||||
messagesCounter='5'
|
selected='files'
|
||||||
filesCounter='10'
|
selectedFilter='code'
|
||||||
isFileAttachmentsEnabled={true}
|
messagesCounter='5'
|
||||||
onChange={jest.fn()}
|
filesCounter='10'
|
||||||
onFilter={jest.fn()}
|
isFileAttachmentsEnabled={true}
|
||||||
/>,
|
onChange={jest.fn()}
|
||||||
|
onFilter={jest.fn()}
|
||||||
|
onTeamChange={jest.fn()}
|
||||||
|
crossTeamSearchEnabled={false}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
test('should match snapshot, without files tab', () => {
|
test('should match snapshot, without files tab', () => {
|
||||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||||
<MessagesOrFilesSelector
|
|
||||||
selected='files'
|
<Provider store={store}>
|
||||||
selectedFilter='code'
|
<MessagesOrFilesSelector
|
||||||
messagesCounter='5'
|
selected='files'
|
||||||
filesCounter='10'
|
selectedFilter='code'
|
||||||
isFileAttachmentsEnabled={false}
|
messagesCounter='5'
|
||||||
onChange={jest.fn()}
|
filesCounter='10'
|
||||||
onFilter={jest.fn()}
|
isFileAttachmentsEnabled={false}
|
||||||
/>,
|
onChange={jest.fn()}
|
||||||
|
onFilter={jest.fn()}
|
||||||
|
onTeamChange={jest.fn()}
|
||||||
|
crossTeamSearchEnabled={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
|
import {getSearchTeam} from 'selectors/rhs';
|
||||||
|
|
||||||
import type {SearchFilterType} from 'components/search/types';
|
import type {SearchFilterType} from 'components/search/types';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
import type {SearchType} from 'types/store/rhs';
|
import type {SearchType} from 'types/store/rhs';
|
||||||
|
|
||||||
import FilesFilterMenu from './files_filter_menu';
|
import FilesFilterMenu from './files_filter_menu';
|
||||||
@@ -23,11 +29,25 @@ type Props = {
|
|||||||
messagesCounter: string;
|
messagesCounter: string;
|
||||||
filesCounter: string;
|
filesCounter: string;
|
||||||
isFileAttachmentsEnabled: boolean;
|
isFileAttachmentsEnabled: boolean;
|
||||||
|
crossTeamSearchEnabled: boolean;
|
||||||
onChange: (value: SearchType) => void;
|
onChange: (value: SearchType) => void;
|
||||||
onFilter: (filter: SearchFilterType) => void;
|
onFilter: (filter: SearchFilterType) => void;
|
||||||
|
onTeamChange: (teamId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||||
|
const teams = useSelector((state: GlobalState) => getMyTeams(state));
|
||||||
|
const searchTeam = useSelector((state: GlobalState) => getSearchTeam(state));
|
||||||
|
|
||||||
|
const options = [{value: '', label: 'All teams', selected: searchTeam === ''}];
|
||||||
|
for (const team of teams) {
|
||||||
|
options.push({value: team.id, label: team.display_name, selected: searchTeam === team.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTeamChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
props.onTeamChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='MessagesOrFilesSelector'>
|
<div className='MessagesOrFilesSelector'>
|
||||||
<div className='buttons-container'>
|
<div className='buttons-container'>
|
||||||
@@ -56,6 +76,23 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{props.crossTeamSearchEnabled && (
|
||||||
|
<div className='team-selector-container'>
|
||||||
|
<select
|
||||||
|
value={searchTeam}
|
||||||
|
onChange={onTeamChange}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.selected === 'files' &&
|
{props.selected === 'files' &&
|
||||||
<FilesFilterMenu
|
<FilesFilterMenu
|
||||||
selectedFilter={props.selectedFilter}
|
selectedFilter={props.selectedFilter}
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSearchTeam = (teamId: string): void => {
|
||||||
|
props.updateSearchTeam(teamId);
|
||||||
|
};
|
||||||
|
|
||||||
const loadMorePosts = debounce(
|
const loadMorePosts = debounce(
|
||||||
() => {
|
() => {
|
||||||
props.getMorePostsForSearch();
|
props.getMorePostsForSearch();
|
||||||
@@ -373,6 +377,8 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
filesCounter={isSearchFilesAtEnd || props.searchPage === 0 ? `${fileResults.length}` : `${fileResults.length}+`}
|
filesCounter={isSearchFilesAtEnd || props.searchPage === 0 ? `${fileResults.length}` : `${fileResults.length}+`}
|
||||||
onChange={setSearchType}
|
onChange={setSearchType}
|
||||||
onFilter={setSearchFilterType}
|
onFilter={setSearchFilterType}
|
||||||
|
onTeamChange={setSearchTeam}
|
||||||
|
crossTeamSearchEnabled={props.crossTeamSearchEnabled}
|
||||||
/>}
|
/>}
|
||||||
{isChannelFiles &&
|
{isChannelFiles &&
|
||||||
<div className='channel-files__header'>
|
<div className='channel-files__header'>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type OwnProps = {
|
|||||||
setSearchType: (searchType: SearchType) => void;
|
setSearchType: (searchType: SearchType) => void;
|
||||||
searchFilterType: SearchFilterType;
|
searchFilterType: SearchFilterType;
|
||||||
setSearchFilterType: (filterType: SearchFilterType) => void;
|
setSearchFilterType: (filterType: SearchFilterType) => void;
|
||||||
|
updateSearchTeam: (teamId: string) => void;
|
||||||
|
crossTeamSearchEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ describe('Reducers.RHS', () => {
|
|||||||
isSidebarExpanded: false,
|
isSidebarExpanded: false,
|
||||||
editChannelMembers: false,
|
editChannelMembers: false,
|
||||||
shouldFocusRHS: false,
|
shouldFocusRHS: false,
|
||||||
|
searchTeam: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Initial state', () => {
|
test('Initial state', () => {
|
||||||
@@ -282,6 +283,21 @@ describe('Reducers.RHS', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should match searchTeam state', () => {
|
||||||
|
const nextState = rhsReducer(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
type: ActionTypes.UPDATE_RHS_SEARCH_TEAM,
|
||||||
|
teamId: 'team_id',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nextState).toEqual({
|
||||||
|
...initialState,
|
||||||
|
searchTeam: 'team_id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should match select_post state', () => {
|
test('should match select_post state', () => {
|
||||||
const nextState1 = rhsReducer(
|
const nextState1 = rhsReducer(
|
||||||
{},
|
{},
|
||||||
@@ -628,6 +644,7 @@ describe('Reducers.RHS', () => {
|
|||||||
isSidebarExpanded: true,
|
isSidebarExpanded: true,
|
||||||
editChannelMembers: false,
|
editChannelMembers: false,
|
||||||
shouldFocusRHS: false,
|
shouldFocusRHS: false,
|
||||||
|
searchTeam: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextState = rhsReducer(state, {type: ActionTypes.SUPPRESS_RHS});
|
const nextState = rhsReducer(state, {type: ActionTypes.SUPPRESS_RHS});
|
||||||
|
|||||||
@@ -209,6 +209,22 @@ function searchTerms(state = '', action: AnyAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchTeam(state = null, action: AnyAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.UPDATE_RHS_SEARCH_TEAM:
|
||||||
|
return action.teamId;
|
||||||
|
case ActionTypes.UPDATE_RHS_STATE:
|
||||||
|
if (action.state !== RHSStates.SEARCH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
case UserTypes.LOGOUT_SUCCESS:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function searchType(state = '', action: AnyAction) {
|
function searchType(state = '', action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.UPDATE_RHS_SEARCH_TYPE:
|
case ActionTypes.UPDATE_RHS_SEARCH_TYPE:
|
||||||
@@ -417,6 +433,7 @@ export default combineReducers({
|
|||||||
filesSearchExtFilter,
|
filesSearchExtFilter,
|
||||||
rhsState,
|
rhsState,
|
||||||
searchTerms,
|
searchTerms,
|
||||||
|
searchTeam,
|
||||||
searchType,
|
searchType,
|
||||||
searchResultsTerms,
|
searchResultsTerms,
|
||||||
searchResultsType,
|
searchResultsType,
|
||||||
|
|||||||
@@ -65,4 +65,22 @@ describe('Selectors.Rhs', () => {
|
|||||||
expect(Selectors.getPreviousRhsState(state)).toEqual(previous);
|
expect(Selectors.getPreviousRhsState(state)).toEqual(previous);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should return the search team', () => {
|
||||||
|
test.each([
|
||||||
|
[undefined, 'currentTeamId'],
|
||||||
|
[null, 'currentTeamId'],
|
||||||
|
['', ''],
|
||||||
|
['searchTeamId', 'searchTeamId'],
|
||||||
|
])('%p gives %p', (searchTeam, expected) => {
|
||||||
|
const state = {
|
||||||
|
entities: {teams: {currentTeamId: 'currentTeamId'}},
|
||||||
|
views: {rhs: {
|
||||||
|
searchTeam,
|
||||||
|
}} as any,
|
||||||
|
} as GlobalState;
|
||||||
|
|
||||||
|
expect(Selectors.getSearchTeam(state)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {Post, PostType} from '@mattermost/types/posts';
|
|||||||
|
|
||||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {getGlobalItem, makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
import {getGlobalItem, makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
||||||
@@ -123,6 +124,11 @@ export function getSearchTerms(state: GlobalState): string {
|
|||||||
return state.views.rhs.searchTerms;
|
return state.views.rhs.searchTerms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getSearchTeam returns the team ID that the search is currently scoped to, or the current team if no team was specified.
|
||||||
|
export function getSearchTeam(state: GlobalState): string {
|
||||||
|
return state.views.rhs.searchTeam ?? getCurrentTeamId(state);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSearchType(state: GlobalState): SearchType {
|
export function getSearchType(state: GlobalState): SearchType {
|
||||||
return state.views.rhs.searchType;
|
return state.views.rhs.searchType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
import type {Post, PostType} from '@mattermost/types/posts';
|
import type {Post, PostType} from '@mattermost/types/posts';
|
||||||
|
import type {Team} from '@mattermost/types/teams';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import type {SidebarSize} from 'components/resizable_sidebar/constants';
|
import type {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
@@ -31,6 +32,7 @@ export type RhsViewState = {
|
|||||||
filesSearchExtFilter: string[];
|
filesSearchExtFilter: string[];
|
||||||
rhsState: RhsState;
|
rhsState: RhsState;
|
||||||
searchTerms: string;
|
searchTerms: string;
|
||||||
|
searchTeam: Team['id'] | null;
|
||||||
searchType: SearchType;
|
searchType: SearchType;
|
||||||
pluggableId: string;
|
pluggableId: string;
|
||||||
searchResultsTerms: string;
|
searchResultsTerms: string;
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const ActionTypes = keyMirror({
|
|||||||
|
|
||||||
UPDATE_RHS_STATE: null,
|
UPDATE_RHS_STATE: null,
|
||||||
UPDATE_RHS_SEARCH_TERMS: null,
|
UPDATE_RHS_SEARCH_TERMS: null,
|
||||||
|
UPDATE_RHS_SEARCH_TEAM: null,
|
||||||
UPDATE_RHS_SEARCH_TYPE: null,
|
UPDATE_RHS_SEARCH_TYPE: null,
|
||||||
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
||||||
UPDATE_RHS_SEARCH_RESULTS_TYPE: null,
|
UPDATE_RHS_SEARCH_RESULTS_TYPE: null,
|
||||||
|
|||||||
@@ -2473,8 +2473,13 @@ export default class Client4 {
|
|||||||
searchFilesWithParams = (teamId: string, params: any) => {
|
searchFilesWithParams = (teamId: string, params: any) => {
|
||||||
this.trackEvent('api', 'api_files_search', {team_id: teamId});
|
this.trackEvent('api', 'api_files_search', {team_id: teamId});
|
||||||
|
|
||||||
|
let route = `${this.getFilesRoute()}/search`;
|
||||||
|
if (teamId) {
|
||||||
|
route = `${this.getTeamRoute(teamId)}/files/search`;
|
||||||
|
}
|
||||||
|
|
||||||
return this.doFetch<FileSearchResults>(
|
return this.doFetch<FileSearchResults>(
|
||||||
`${this.getTeamRoute(teamId)}/files/search`,
|
route,
|
||||||
{method: 'post', body: JSON.stringify(params)},
|
{method: 'post', body: JSON.stringify(params)},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user