[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,6 +39,7 @@ import {
|
||||
goBack,
|
||||
showChannelMembers,
|
||||
openShowEditHistory,
|
||||
updateSearchTeam,
|
||||
} from 'actions/views/rhs';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
@@ -229,7 +230,7 @@ describe('rhs view actions', () => {
|
||||
|
||||
test('it dispatches searchPosts correctly', () => {
|
||||
const terms = '@here test search';
|
||||
store.dispatch(performSearch(terms, false));
|
||||
store.dispatch(performSearch(terms, currentTeamId, false));
|
||||
|
||||
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}));
|
||||
@@ -251,7 +252,7 @@ describe('rhs view actions', () => {
|
||||
});
|
||||
|
||||
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 compareStore = mockStore(initialState);
|
||||
@@ -263,12 +264,12 @@ describe('rhs view actions', () => {
|
||||
|
||||
test('it dispatches searchPosts correctly for Recent Mentions', () => {
|
||||
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 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.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());
|
||||
});
|
||||
@@ -282,6 +283,7 @@ describe('rhs view actions', () => {
|
||||
views: {
|
||||
rhs: {
|
||||
searchTerms: terms,
|
||||
searchTeam: null,
|
||||
searchType: 'messages',
|
||||
filesSearchExtFilter: [] as string[],
|
||||
},
|
||||
@@ -303,7 +305,7 @@ describe('rhs view actions', () => {
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||
searchType: 'messages',
|
||||
});
|
||||
compareStore.dispatch(performSearch(terms));
|
||||
compareStore.dispatch(performSearch(terms, currentTeamId));
|
||||
|
||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||
});
|
||||
@@ -483,7 +485,7 @@ describe('rhs view actions', () => {
|
||||
|
||||
const compareStore = mockStore(initialState);
|
||||
|
||||
compareStore.dispatch(performSearch('@mattermost ', true));
|
||||
compareStore.dispatch(performSearch('@mattermost ', '', true));
|
||||
compareStore.dispatch(batchActions([
|
||||
{
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||
@@ -740,6 +742,7 @@ describe('rhs view actions', () => {
|
||||
|
||||
compareStore.dispatch(SearchActions.clearSearch());
|
||||
compareStore.dispatch(updateSearchTerms(''));
|
||||
compareStore.dispatch(updateSearchTeam(null));
|
||||
compareStore.dispatch({
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS,
|
||||
terms: '',
|
||||
@@ -759,7 +762,7 @@ describe('rhs view actions', () => {
|
||||
store.dispatch(openAtPrevious({isMentionSearch: true}));
|
||||
const compareStore = mockStore(initialState);
|
||||
|
||||
compareStore.dispatch(performSearch('@mattermost ', true));
|
||||
compareStore.dispatch(performSearch('@mattermost ', '', true));
|
||||
compareStore.dispatch(batchActions([
|
||||
{
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||
@@ -872,6 +875,7 @@ describe('rhs view actions', () => {
|
||||
views: {
|
||||
rhs: {
|
||||
searchTerms: terms,
|
||||
searchTeam: null,
|
||||
searchType: 'messages',
|
||||
filesSearchExtFilter: [] as string[],
|
||||
},
|
||||
@@ -891,7 +895,8 @@ describe('rhs view actions', () => {
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||
searchType: 'messages',
|
||||
});
|
||||
compareStore.dispatch(performSearch(terms));
|
||||
|
||||
compareStore.dispatch(performSearch(terms, currentTeamId));
|
||||
|
||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
getPluggableId,
|
||||
getFilesSearchExtFilter,
|
||||
getPreviousRhsState,
|
||||
getSearchTeam,
|
||||
} from 'selectors/rhs';
|
||||
|
||||
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) {
|
||||
let newSidebarSize = rhsSize;
|
||||
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) => {
|
||||
let searchTerms = terms;
|
||||
const teamId = getCurrentTeamId(getState());
|
||||
const config = getConfig(getState());
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
const extensionsFilters = getFilesSearchExtFilter(getState());
|
||||
@@ -236,7 +243,7 @@ export function performSearch(terms: string, isMentionSearch?: boolean): ThunkAc
|
||||
// timezone offset in seconds
|
||||
const userCurrentTimezone = getCurrentTimezone(getState());
|
||||
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}));
|
||||
return Promise.all([filesPromise, messagesPromise]);
|
||||
};
|
||||
@@ -254,17 +261,19 @@ export function showSearchResults(isMentionSearch = false): ThunkActionFunc<unkn
|
||||
const state = getState();
|
||||
|
||||
const searchTerms = getSearchTerms(state);
|
||||
let teamId = getSearchTeam(state);
|
||||
const searchType = getSearchType(state);
|
||||
|
||||
if (isMentionSearch) {
|
||||
dispatch(updateRhsState(RHSStates.MENTION));
|
||||
teamId = '';
|
||||
} else {
|
||||
dispatch(updateRhsState(RHSStates.SEARCH));
|
||||
}
|
||||
dispatch(updateSearchResultsTerms(searchTerms));
|
||||
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> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const teamId = getCurrentTeamId(state);
|
||||
const teamId = getSearchTeam(state);
|
||||
|
||||
let previousRhsState = getRhsState(state);
|
||||
if (previousRhsState === RHSStates.CHANNEL_FILES) {
|
||||
@@ -420,7 +429,7 @@ export function showChannelFiles(channelId: string): ActionFuncAsync<boolean, Gl
|
||||
});
|
||||
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 missingPostIds: string[] = [];
|
||||
|
||||
@@ -471,7 +480,7 @@ export function showMentions(): ActionFunc<boolean, GlobalState> {
|
||||
|
||||
trackEvent('api', 'api_posts_search_mention');
|
||||
|
||||
dispatch(performSearch(terms, true));
|
||||
dispatch(performSearch(terms, '', true));
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
|
||||
@@ -604,6 +613,7 @@ export function openRHSSearch(): ActionFunc {
|
||||
return (dispatch) => {
|
||||
dispatch(clearSearch());
|
||||
dispatch(updateSearchTerms(''));
|
||||
dispatch(updateSearchTeam(null));
|
||||
dispatch(updateSearchResultsTerms(''));
|
||||
|
||||
dispatch(updateRhsState(RHSStates.SEARCH));
|
||||
|
||||
@@ -7,11 +7,13 @@ import type {Dispatch} from 'redux';
|
||||
|
||||
import {getMorePostsForSearch, getMoreFilesForSearch} from 'mattermost-redux/actions/search';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {autocompleteChannelsForSearch} from 'actions/channel_actions';
|
||||
import {autocompleteUsersInTeam} from 'actions/user_actions';
|
||||
import {
|
||||
updateSearchTerms,
|
||||
updateSearchTeam,
|
||||
updateSearchTermsForShortcut,
|
||||
showSearchResults,
|
||||
showChannelFiles,
|
||||
@@ -24,7 +26,7 @@ import {
|
||||
filterFilesSearchByExt,
|
||||
updateSearchType,
|
||||
} 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 {RHSStates} from 'utils/constants';
|
||||
@@ -39,12 +41,18 @@ function mapStateToProps(state: GlobalState) {
|
||||
const isMobileView = getIsMobileView(state);
|
||||
const isRhsOpen = getIsRhsOpen(state);
|
||||
|
||||
let searchTeam = getSearchTeam(state);
|
||||
if (!searchTeam) {
|
||||
searchTeam = currentChannel?.team_id || '';
|
||||
}
|
||||
|
||||
return {
|
||||
currentChannel,
|
||||
isRhsExpanded: getIsRhsExpanded(state),
|
||||
isRhsOpen,
|
||||
isSearchingTerm: getIsSearchingTerm(state),
|
||||
searchTerms: getSearchTerms(state),
|
||||
searchTeam,
|
||||
searchType: getSearchType(state),
|
||||
searchVisible: rhsState !== null && (![
|
||||
RHSStates.PLUGIN,
|
||||
@@ -58,6 +66,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
isPinnedPosts: rhsState === RHSStates.PIN,
|
||||
isChannelFiles: rhsState === RHSStates.CHANNEL_FILES,
|
||||
isMobileView,
|
||||
crossTeamSearchEnabled: getFeatureFlagValue(state, 'ExperimentalCrossTeamSearch') === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,6 +74,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
updateSearchTerms,
|
||||
updateSearchTeam,
|
||||
updateSearchTermsForShortcut,
|
||||
updateSearchType,
|
||||
showSearchResults,
|
||||
|
||||
@@ -207,6 +207,14 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
handleUpdateSearchTerms(pretextArray.join(' '));
|
||||
};
|
||||
|
||||
const handleUpdateSearchTeam = async (teamId: string) => {
|
||||
actions.updateSearchTeam(teamId);
|
||||
handleSearch().then(() => {
|
||||
setKeepInputFocused(false);
|
||||
setFocused(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateSearchTerms = (terms: string): void => {
|
||||
actions.updateSearchTerms(terms);
|
||||
updateHighlightedSearchHint();
|
||||
@@ -310,6 +318,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
actions.updateRhsState(RHSStates.SEARCH);
|
||||
}
|
||||
actions.updateSearchTerms('');
|
||||
actions.updateSearchTeam(null);
|
||||
actions.updateSearchType('');
|
||||
};
|
||||
|
||||
@@ -544,6 +553,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
channelDisplayName={props.channelDisplayName}
|
||||
isOpened={props.isSideBarRightOpen}
|
||||
updateSearchTerms={handleAddSearchTerm}
|
||||
updateSearchTeam={handleUpdateSearchTeam}
|
||||
handleSearchHintSelection={handleSearchHintSelection}
|
||||
isSideBarExpanded={props.isRhsExpanded}
|
||||
getMorePostsForSearch={props.actions.getMorePostsForSearch}
|
||||
@@ -552,6 +562,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
searchFilterType={searchFilterType}
|
||||
setSearchType={(value: SearchType) => actions.updateSearchType(value)}
|
||||
searchType={searchType || 'messages'}
|
||||
crossTeamSearchEnabled={props.crossTeamSearchEnabled}
|
||||
/>
|
||||
) : props.children}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ export type StateProps = {
|
||||
isRhsOpen: boolean;
|
||||
isSearchingTerm: boolean;
|
||||
searchTerms: string;
|
||||
searchTeam: string;
|
||||
searchType: SearchType;
|
||||
searchVisible: boolean;
|
||||
hideMobileSearchBarInRHS: boolean;
|
||||
@@ -36,11 +37,13 @@ export type StateProps = {
|
||||
isChannelFiles: boolean;
|
||||
currentChannel?: Channel;
|
||||
isMobileView: boolean;
|
||||
crossTeamSearchEnabled: boolean;
|
||||
}
|
||||
|
||||
export type DispatchProps = {
|
||||
actions: {
|
||||
updateSearchTerms: (term: string) => Action;
|
||||
updateSearchTeam: (teamId: string|null) => Action;
|
||||
updateSearchTermsForShortcut: () => void;
|
||||
updateSearchType: (searchType: string) => Action;
|
||||
showSearchResults: (isMentionSearch: boolean) => unknown;
|
||||
|
||||
@@ -1,117 +1,139 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on files selected 1`] = `
|
||||
<div
|
||||
className="MessagesOrFilesSelector"
|
||||
<ContextProvider
|
||||
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
|
||||
className="buttons-container"
|
||||
>
|
||||
<button
|
||||
className="tab messages-tab"
|
||||
onClick={[Function]}
|
||||
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
|
||||
<MessagesOrFilesSelector
|
||||
crossTeamSearchEnabled={false}
|
||||
filesCounter="10"
|
||||
isFileAttachmentsEnabled={true}
|
||||
messagesCounter="5"
|
||||
onChange={[MockFunction]}
|
||||
onFilter={[MockFunction]}
|
||||
onTeamChange={[MockFunction]}
|
||||
selected="files"
|
||||
selectedFilter="code"
|
||||
/>
|
||||
</div>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, on messages selected 1`] = `
|
||||
<div
|
||||
className="MessagesOrFilesSelector"
|
||||
<ContextProvider
|
||||
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
|
||||
className="buttons-container"
|
||||
>
|
||||
<button
|
||||
className="active tab messages-tab"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Messages"
|
||||
id="search_bar.messages_tab"
|
||||
/>
|
||||
<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>
|
||||
<MessagesOrFilesSelector
|
||||
crossTeamSearchEnabled={false}
|
||||
filesCounter="10"
|
||||
isFileAttachmentsEnabled={true}
|
||||
messagesCounter="5"
|
||||
onChange={[MockFunction]}
|
||||
onFilter={[MockFunction]}
|
||||
onTeamChange={[MockFunction]}
|
||||
selected="messages"
|
||||
selectedFilter="code"
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/search_results/MessagesOrFilesSelector should match snapshot, without files tab 1`] = `
|
||||
<div
|
||||
className="MessagesOrFilesSelector"
|
||||
<ContextProvider
|
||||
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
|
||||
className="buttons-container"
|
||||
>
|
||||
<button
|
||||
className="tab messages-tab"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Messages"
|
||||
id="search_bar.messages_tab"
|
||||
/>
|
||||
<span
|
||||
className="counter"
|
||||
>
|
||||
5
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<FilesFilterMenu
|
||||
<MessagesOrFilesSelector
|
||||
crossTeamSearchEnabled={false}
|
||||
filesCounter="10"
|
||||
isFileAttachmentsEnabled={false}
|
||||
messagesCounter="5"
|
||||
onChange={[MockFunction]}
|
||||
onFilter={[MockFunction]}
|
||||
onTeamChange={[MockFunction]}
|
||||
selected="files"
|
||||
selectedFilter="code"
|
||||
/>
|
||||
</div>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.MessagesOrFilesSelector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.buttons-container {
|
||||
flex-grow: 1;
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,30 @@
|
||||
import {shallow} from 'enzyme';
|
||||
import type {ShallowWrapper} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import MessagesOrFilesSelector from 'components/search_results/messages_or_files_selector';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
describe('components/search_results/MessagesOrFilesSelector', () => {
|
||||
const store = mockStore({});
|
||||
|
||||
test('should match snapshot, on messages selected', () => {
|
||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||
<MessagesOrFilesSelector
|
||||
selected='messages'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={true}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<MessagesOrFilesSelector
|
||||
selected='messages'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={true}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
onTeamChange={jest.fn()}
|
||||
crossTeamSearchEnabled={false}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -26,30 +35,41 @@ describe('components/search_results/MessagesOrFilesSelector', () => {
|
||||
|
||||
test('should match snapshot, on files selected', () => {
|
||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||
<MessagesOrFilesSelector
|
||||
selected='files'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={true}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
/>,
|
||||
|
||||
<Provider store={store}>
|
||||
<MessagesOrFilesSelector
|
||||
selected='files'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={true}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
onTeamChange={jest.fn()}
|
||||
crossTeamSearchEnabled={false}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('should match snapshot, without files tab', () => {
|
||||
const wrapper: ShallowWrapper<any, any, any> = shallow(
|
||||
<MessagesOrFilesSelector
|
||||
selected='files'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={false}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
/>,
|
||||
|
||||
<Provider store={store}>
|
||||
<MessagesOrFilesSelector
|
||||
selected='files'
|
||||
selectedFilter='code'
|
||||
messagesCounter='5'
|
||||
filesCounter='10'
|
||||
isFileAttachmentsEnabled={false}
|
||||
onChange={jest.fn()}
|
||||
onFilter={jest.fn()}
|
||||
onTeamChange={jest.fn()}
|
||||
crossTeamSearchEnabled={false}
|
||||
/>
|
||||
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
|
||||
import React from 'react';
|
||||
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 Constants from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
import type {SearchType} from 'types/store/rhs';
|
||||
|
||||
import FilesFilterMenu from './files_filter_menu';
|
||||
@@ -23,11 +29,25 @@ type Props = {
|
||||
messagesCounter: string;
|
||||
filesCounter: string;
|
||||
isFileAttachmentsEnabled: boolean;
|
||||
crossTeamSearchEnabled: boolean;
|
||||
onChange: (value: SearchType) => void;
|
||||
onFilter: (filter: SearchFilterType) => void;
|
||||
onTeamChange: (teamId: string) => void;
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className='MessagesOrFilesSelector'>
|
||||
<div className='buttons-container'>
|
||||
@@ -56,6 +76,23 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
</button>
|
||||
}
|
||||
</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' &&
|
||||
<FilesFilterMenu
|
||||
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(
|
||||
() => {
|
||||
props.getMorePostsForSearch();
|
||||
@@ -373,6 +377,8 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
filesCounter={isSearchFilesAtEnd || props.searchPage === 0 ? `${fileResults.length}` : `${fileResults.length}+`}
|
||||
onChange={setSearchType}
|
||||
onFilter={setSearchFilterType}
|
||||
onTeamChange={setSearchTeam}
|
||||
crossTeamSearchEnabled={props.crossTeamSearchEnabled}
|
||||
/>}
|
||||
{isChannelFiles &&
|
||||
<div className='channel-files__header'>
|
||||
|
||||
@@ -29,6 +29,8 @@ export type OwnProps = {
|
||||
setSearchType: (searchType: SearchType) => void;
|
||||
searchFilterType: SearchFilterType;
|
||||
setSearchFilterType: (filterType: SearchFilterType) => void;
|
||||
updateSearchTeam: (teamId: string) => void;
|
||||
crossTeamSearchEnabled: boolean;
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('Reducers.RHS', () => {
|
||||
isSidebarExpanded: false,
|
||||
editChannelMembers: false,
|
||||
shouldFocusRHS: false,
|
||||
searchTeam: null,
|
||||
};
|
||||
|
||||
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', () => {
|
||||
const nextState1 = rhsReducer(
|
||||
{},
|
||||
@@ -628,6 +644,7 @@ describe('Reducers.RHS', () => {
|
||||
isSidebarExpanded: true,
|
||||
editChannelMembers: false,
|
||||
shouldFocusRHS: false,
|
||||
searchTeam: null,
|
||||
};
|
||||
|
||||
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) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UPDATE_RHS_SEARCH_TYPE:
|
||||
@@ -417,6 +433,7 @@ export default combineReducers({
|
||||
filesSearchExtFilter,
|
||||
rhsState,
|
||||
searchTerms,
|
||||
searchTeam,
|
||||
searchType,
|
||||
searchResultsTerms,
|
||||
searchResultsType,
|
||||
|
||||
@@ -65,4 +65,22 @@ describe('Selectors.Rhs', () => {
|
||||
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 {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getGlobalItem, makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
||||
@@ -123,6 +124,11 @@ export function getSearchTerms(state: GlobalState): string {
|
||||
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 {
|
||||
return state.views.rhs.searchType;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post, PostType} from '@mattermost/types/posts';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
@@ -31,6 +32,7 @@ export type RhsViewState = {
|
||||
filesSearchExtFilter: string[];
|
||||
rhsState: RhsState;
|
||||
searchTerms: string;
|
||||
searchTeam: Team['id'] | null;
|
||||
searchType: SearchType;
|
||||
pluggableId: string;
|
||||
searchResultsTerms: string;
|
||||
|
||||
@@ -195,6 +195,7 @@ export const ActionTypes = keyMirror({
|
||||
|
||||
UPDATE_RHS_STATE: null,
|
||||
UPDATE_RHS_SEARCH_TERMS: null,
|
||||
UPDATE_RHS_SEARCH_TEAM: null,
|
||||
UPDATE_RHS_SEARCH_TYPE: null,
|
||||
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
||||
UPDATE_RHS_SEARCH_RESULTS_TYPE: null,
|
||||
|
||||
@@ -2473,8 +2473,13 @@ export default class Client4 {
|
||||
searchFilesWithParams = (teamId: string, params: any) => {
|
||||
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>(
|
||||
`${this.getTeamRoute(teamId)}/files/search`,
|
||||
route,
|
||||
{method: 'post', body: JSON.stringify(params)},
|
||||
);
|
||||
};
|
||||
|
||||
Ссылка в новой задаче
Block a user