diff --git a/app/components/team_list/index.test.tsx b/app/components/team_list/index.test.tsx new file mode 100644 index 00000000000..7a510c1fc48 --- /dev/null +++ b/app/components/team_list/index.test.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {fireEvent, renderWithEverything} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import TeamList from './index'; + +import type Database from '@nozbe/watermelondb/Database'; +import type TeamModel from '@typings/database/models/servers/team'; + +describe('TeamList', () => { + let database: Database; + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + database = server.database; + }); + const teams = [ + {id: 'team1', displayName: 'Team 1'} as TeamModel, + {id: 'team2', displayName: 'Team 2'} as TeamModel, + ]; + + it('should call onPress when a team is pressed', () => { + const onPress = jest.fn(); + const {getByText} = renderWithEverything( + , + {database}, + ); + fireEvent.press(getByText('Team 1')); + expect(onPress).toHaveBeenCalledWith('team1'); + }); + + it('should render loading component when loading is true', () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + expect(getByTestId('team_list.loading')).toBeTruthy(); + }); + + it('should render separator after the first item when separatorAfterFirstItem is true', () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + expect(getByTestId('team_list.separator')).toBeTruthy(); + }); +}); diff --git a/app/components/team_list/index.tsx b/app/components/team_list/index.tsx index b1e5e412468..f107812e13f 100644 --- a/app/components/team_list/index.tsx +++ b/app/components/team_list/index.tsx @@ -3,10 +3,12 @@ import {BottomSheetFlatList} from '@gorhom/bottom-sheet'; import React, {useCallback, useMemo} from 'react'; -import {type ListRenderItemInfo, StyleSheet, View} from 'react-native'; +import {type ListRenderItemInfo, View} from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; // Keep the FlatList from gesture handler so it works well with bottom sheet import Loading from '@components/loading'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import TeamListItem from './team_list_item'; @@ -23,15 +25,23 @@ type Props = { testID?: string; textColor?: string; type?: BottomSheetList; + hideIcon?: boolean; + separatorAfterFirstItem?: boolean; } -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - }, - contentContainer: { - marginBottom: 4, - }, +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flexGrow: 1, + }, + contentContainer: { + marginBottom: 4, + }, + separator: { + height: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + }, + }; }); const keyExtractor = (item: TeamModel) => item.id; @@ -47,11 +57,15 @@ export default function TeamList({ testID, textColor, type = 'FlatList', + hideIcon = false, + separatorAfterFirstItem = false, }: Props) { const List = useMemo(() => (type === 'FlatList' ? FlatList : BottomSheetFlatList), [type]); + const theme = useTheme(); + const styles = getStyleSheet(theme); - const renderTeam = useCallback(({item: t}: ListRenderItemInfo) => { - return ( + const renderTeam = useCallback(({item: t, index: i}: ListRenderItemInfo) => { + let teamListItem = ( ); - }, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]); + if (separatorAfterFirstItem && i === 0) { + teamListItem = (<> + {teamListItem} + + ); + } + return teamListItem; + }, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId, hideIcon, separatorAfterFirstItem, styles.separator]); let footer; if (loading) { - footer = (); + footer = (); } return ( diff --git a/app/components/team_list/team_list_item/team_list_item.test.tsx b/app/components/team_list/team_list_item/team_list_item.test.tsx new file mode 100644 index 00000000000..21524344b31 --- /dev/null +++ b/app/components/team_list/team_list_item/team_list_item.test.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {renderWithIntlAndTheme, fireEvent} from '@test/intl-test-helper'; + +import TeamListItem from './team_list_item'; + +import type TeamModel from '@typings/database/models/servers/team'; + +describe('TeamListItem', () => { + const team = { + id: 'team_id', + displayName: 'Team Display Name', + lastTeamIconUpdatedAt: 0, + } as TeamModel; + const iconTestId = `team_sidebar.team_list.team_list_item.${team.id}.team_icon`; + + it('should call onPress when pressed', () => { + const onPressMock = jest.fn(); + const {getByText} = renderWithIntlAndTheme( + , + ); + + fireEvent.press(getByText('Team Display Name')); + + expect(onPressMock).toHaveBeenCalledWith('team_id'); + }); + + it('should render TeamIcon when hideIcon is false', () => { + const {getByTestId} = renderWithIntlAndTheme( + , + ); + + expect(getByTestId(iconTestId)).toBeTruthy(); + }); + + it('should not render TeamIcon when hideIcon is true', () => { + const {queryByTestId} = renderWithIntlAndTheme( + , + ); + + expect(queryByTestId(iconTestId)).toBeNull(); + }); +}); diff --git a/app/components/team_list/team_list_item/team_list_item.tsx b/app/components/team_list/team_list_item/team_list_item.tsx index a903e25beb1..eb012c743b2 100644 --- a/app/components/team_list/team_list_item/team_list_item.tsx +++ b/app/components/team_list/team_list_item/team_list_item.tsx @@ -20,6 +20,7 @@ type Props = { iconBackgroundColor?: string; selectedTeamId?: string; onPress: (teamId: string) => void; + hideIcon?: boolean; } export const ITEM_HEIGHT = 56; @@ -50,7 +51,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -export default function TeamListItem({team, textColor, iconTextColor, iconBackgroundColor, selectedTeamId, onPress}: Props) { +export default function TeamListItem({team, textColor, iconTextColor, iconBackgroundColor, selectedTeamId, onPress, hideIcon}: Props) { const theme = useTheme(); const styles = getStyleSheet(theme); @@ -68,17 +69,19 @@ export default function TeamListItem({team, textColor, iconTextColor, iconBackgr type='opacity' style={styles.touchable} > - - - + {!hideIcon && + + + + } void; title: string; + crossTeamSearchEnabled: boolean; } -export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: Props) { +export default function BottomSheetTeamList({teams, title, setTeamId, teamId, crossTeamSearchEnabled}: Props) { + const intl = useIntl(); const isTablet = useIsTablet(); const showTitle = !isTablet && Boolean(teams.length); @@ -26,6 +30,14 @@ export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: P dismissBottomSheet(); }, [setTeamId]); + const teamList = useMemo(() => { + const list = [...teams]; + if (crossTeamSearchEnabled) { + list.unshift({id: ALL_TEAMS_ID, displayName: intl.formatMessage({id: 'mobile.search.team.all_teams', defaultMessage: 'All teams'})} as TeamModel); + } + return list; + }, [teams, crossTeamSearchEnabled, intl]); + return ( ); diff --git a/app/screens/home/search/index.tsx b/app/screens/home/search/index.tsx index 7240fd2b7b0..5ed7666785c 100644 --- a/app/screens/home/search/index.tsx +++ b/app/screens/home/search/index.tsx @@ -4,7 +4,7 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react'; import compose from 'lodash/fp/compose'; -import {observeCurrentTeamId} from '@queries/servers/system'; +import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system'; import {queryJoinedTeams} from '@queries/servers/team'; import SearchScreen from './search'; @@ -16,6 +16,7 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => { return { teamId: currentTeamId, teams: queryJoinedTeams(database).observe(), + crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'), }; }); diff --git a/app/screens/home/search/initial/index.tsx b/app/screens/home/search/initial/index.tsx index 2782170eb71..10fe9d14730 100644 --- a/app/screens/home/search/initial/index.tsx +++ b/app/screens/home/search/initial/index.tsx @@ -5,6 +5,7 @@ import compose from 'lodash/fp/compose'; import {of as of$} from 'rxjs'; import {switchMap, distinctUntilChanged} from 'rxjs/operators'; +import {observeConfigBooleanValue} from '@queries/servers/system'; import {observeTeam, queryTeamSearchHistoryByTeamId} from '@queries/servers/team'; import Initial from './initial'; @@ -22,6 +23,7 @@ const enhance = withObservables(['teamId'], ({database, teamId}: EnhanceProps) = switchMap((t) => of$(t?.displayName || '')), distinctUntilChanged(), ), + crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'), }; }); diff --git a/app/screens/home/search/initial/initial.tsx b/app/screens/home/search/initial/initial.tsx index 3a70ccf02db..6053bd18eee 100644 --- a/app/screens/home/search/initial/initial.tsx +++ b/app/screens/home/search/initial/initial.tsx @@ -3,6 +3,8 @@ import React, {type Dispatch, type RefObject, type SetStateAction} from 'react'; +import {ALL_TEAMS_ID} from '@constants/team'; + import Modifiers from './modifiers'; import RecentSearches from './recent_searches'; @@ -22,9 +24,11 @@ type Props = { teamId: string; teamName: string; teams: TeamModel[]; + crossTeamSearchEnabled: boolean; } -const Initial = ({recentSearches, scrollEnabled, searchValue, setRecentValue, searchRef, teamId, teamName, teams, setTeamId, setSearchValue}: Props) => { +const Initial = ({recentSearches, scrollEnabled, searchValue, setRecentValue, searchRef, teamId, teamName, teams, setTeamId, setSearchValue, crossTeamSearchEnabled}: Props) => { + const showRecentSearches = Boolean(recentSearches.length) && teamId !== ALL_TEAMS_ID; return ( <> - {Boolean(recentSearches.length) && + {showRecentSearches && jest.fn(() => null)); +jest.mock('./show_more', () => jest.fn(() => null)); +jest.mock('@react-native-camera-roll/camera-roll', () => jest.fn()); + +describe('Modifiers', () => { + const scrollEnabled = useSharedValue(true); + const searchRef = React.createRef(); + const setSearchValue = jest.fn(); + const setTeamId = jest.fn(); + const teams = [{id: 'team1', displayName: 'Team 1'}, {id: 'team2', displayName: 'Team 2'}] as TeamModel[]; + + const renderComponent = (teamId = ALL_TEAMS_ID) => { + return renderWithIntlAndTheme( + , + ); + }; + + it('should render correctly', () => { + const {getByTestId} = renderComponent(); + expect(getByTestId('search.modifier.header')).toBeTruthy(); + }); + + it('should render TeamPicker when there are multiple teams', () => { + renderComponent(); + expect(TeamPicker).toHaveBeenCalled(); + }); + + it('should render the From: and In: modifiers when a team is selected', () => { + const {getByTestId} = renderComponent('team1'); + expect(getByTestId('search.modifier.from')).toBeTruthy(); + expect(getByTestId('search.modifier.in')).toBeTruthy(); + }); + + it('should not render the From: and In: modifiers when all teams are selected', () => { + const {queryByTestId} = renderComponent(ALL_TEAMS_ID); + expect(queryByTestId('search.modifier.from')).toBeNull(); + expect(queryByTestId('search.modifier.in')).toBeNull(); + }); +}); diff --git a/app/screens/home/search/initial/modifiers/index.tsx b/app/screens/home/search/initial/modifiers/index.tsx index f2028d9a571..a08cfe4955d 100644 --- a/app/screens/home/search/initial/modifiers/index.tsx +++ b/app/screens/home/search/initial/modifiers/index.tsx @@ -4,11 +4,12 @@ import React, {type Dispatch, type RefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {type IntlShape, useIntl} from 'react-intl'; import {View} from 'react-native'; -import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; +import Animated, {type SharedValue, useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; import FormattedText from '@components/formatted_text'; +import {ALL_TEAMS_ID} from '@constants/team'; import {useTheme} from '@context/theme'; -import TeamPickerIcon from '@screens/home/search/team_picker_icon'; +import TeamPicker from '@screens/home/search/team_picker'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -19,31 +20,37 @@ import type {SearchRef} from '@components/search'; import type TeamModel from '@typings/database/models/servers/team'; const MODIFIER_LABEL_HEIGHT = 48; -const TEAM_PICKER_ICON_SIZE = 32; const NUM_ITEMS_BEFORE_EXPAND = 4; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { - titleContainer: { + header: { alignItems: 'center', flexDirection: 'row', marginTop: 20, - marginRight: 18, + marginHorizontal: 18, }, - title: { + titleContainer: { flex: 1, + }, + title: { alignItems: 'center', - paddingLeft: 18, color: theme.centerChannelColor, ...typography('Heading', 300, 'SemiBold'), }, + teamPickerContainer: { + flex: 1, + alignItems: 'flex-end', + }, }; }); -const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => { +const getModifiersSectionsData = (intl: IntlShape, teamId: string): ModifierItem[] => { const formatMessage = intl.formatMessage; - const sectionsData = [ - { + + const sectionsData = []; + if (teamId !== ALL_TEAMS_ID) { + sectionsData.push({ term: 'From:', testID: 'search.modifier.from', description: formatMessage({id: 'mobile.search.modifier.from', defaultMessage: ' a specific user'}), @@ -51,52 +58,40 @@ const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => { term: 'In:', testID: 'search.modifier.in', description: formatMessage({id: 'mobile.search.modifier.in', defaultMessage: ' a specific channel'}), - }, + }); + } + + sectionsData.push({ + term: '-', + testID: 'search.modifier.exclude', + description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: ' exclude search terms'}), + }, { + term: '""', + testID: 'search.modifier.phrases', + description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: ' messages with phrases'}), + cursorPosition: -1, + }); - // { - // term: 'On:', - // testID: 'search.modifier.on', - // description: formatMessage({id: 'mobile.search.modifier.on', defaultMessage: ' a specific date'}), - // }, - // { - // term: 'After:', - // testID: 'search.modifier.after', - // description: formatMessage({id: 'mobile.search.modifier.after', defaultMessage: ' after a date'}), - // }, { - // term: 'Before:', - // testID: 'search.modifier.before', - // description: formatMessage({id: 'mobile.search.modifier.before', defaultMessage: ' before a date'}), - // }, - { - term: '-', - testID: 'search.modifier.exclude', - description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: ' exclude search terms'}), - }, { - term: '""', - testID: 'search.modifier.phrases', - description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: ' messages with phrases'}), - cursorPosition: -1, - }, - ]; return sectionsData; }; type Props = { - scrollEnabled: Animated.SharedValue; + scrollEnabled: SharedValue; searchRef: RefObject; setSearchValue: Dispatch>; searchValue?: string; setTeamId: (id: string) => void; teamId: string; teams: TeamModel[]; + crossTeamSearchEnabled: boolean; } -const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTeamId, teamId, teams}: Props) => { +const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTeamId, teamId, teams, crossTeamSearchEnabled}: Props) => { const theme = useTheme(); const intl = useIntl(); const [showMore, setShowMore] = useState(false); const height = useSharedValue(NUM_ITEMS_BEFORE_EXPAND * MODIFIER_LABEL_HEIGHT); - const data = useMemo(() => getModifiersSectionsData(intl), [intl]); + const data = useMemo(() => getModifiersSectionsData(intl, teamId), [intl, teamId]); const timeoutRef = useRef(); const styles = getStyleFromTheme(theme); @@ -143,20 +138,25 @@ const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTe return ( <> - - + + + + + {teams.length > 1 && - + + + } diff --git a/app/screens/home/search/results/header.test.tsx b/app/screens/home/search/results/header.test.tsx new file mode 100644 index 00000000000..ec1a44da3a5 --- /dev/null +++ b/app/screens/home/search/results/header.test.tsx @@ -0,0 +1,114 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {renderWithIntlAndTheme, fireEvent} from '@test/intl-test-helper'; +import {FileFilters} from '@utils/file'; +import {TabTypes} from '@utils/search'; + +import Header from './header'; + +import type TeamModel from '@typings/database/models/servers/team'; + +// Some subcomponents require react-native-camera-roll, which is not available in the test environment +jest.mock('@react-native-camera-roll/camera-roll', () => ({})); + +describe('Header', () => { + const onTabSelect = jest.fn(); + const onFilterChanged = jest.fn(); + const setTeamId = jest.fn(); + + const teams = [ + {id: 'team1', displayName: 'Team 1'}, + {id: 'team2', displayName: 'Team 2'}, + ] as TeamModel[]; + + it('should render correctly', () => { + const {getByText} = renderWithIntlAndTheme( +
, + ); + + expect(getByText('Messages')).toBeTruthy(); + expect(getByText('Files')).toBeTruthy(); + }); + + it('should call onTabSelect with MESSAGES when Messages button is pressed', () => { + const {getByText} = renderWithIntlAndTheme( +
, + ); + + fireEvent.press(getByText('Messages')); + expect(onTabSelect).toHaveBeenCalledWith(TabTypes.MESSAGES); + }); + + it('should call onTabSelect with FILES when Files button is pressed', () => { + const {getByText} = renderWithIntlAndTheme( +
, + ); + + fireEvent.press(getByText('Files')); + expect(onTabSelect).toHaveBeenCalledWith(TabTypes.FILES); + }); + + it('should render TeamPicker when there are multiple teams', () => { + const {getByText} = renderWithIntlAndTheme( +
, + ); + + expect(getByText('Team 1')).toBeTruthy(); + }); + + it('should render the file type filter when the Files tab is selected', () => { + const {getByTestId} = renderWithIntlAndTheme( +
, + ); + + expect(getByTestId('search.filters.file_type_icon')).toBeTruthy(); + }); +}); diff --git a/app/screens/home/search/results/header.tsx b/app/screens/home/search/results/header.tsx index e989ecd38a5..76fe8ba6bca 100644 --- a/app/screens/home/search/results/header.tsx +++ b/app/screens/home/search/results/header.tsx @@ -10,7 +10,7 @@ import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, NUMBER_FILTER_ITEMS} from ' import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {TITLE_SEPARATOR_MARGIN, TITLE_SEPARATOR_MARGIN_TABLET, TITLE_HEIGHT} from '@screens/bottom_sheet/content'; -import TeamPickerIcon from '@screens/home/search/team_picker_icon'; +import TeamPicker from '@screens/home/search/team_picker'; import {bottomSheet} from '@screens/navigation'; import {type FileFilter, FileFilters} from '@utils/file'; import {bottomSheetSnapPoint} from '@utils/helpers'; @@ -29,13 +29,13 @@ type Props = { setTeamId: (id: string) => void; teamId: string; teams: TeamModel[]; + crossTeamSearchEnabled: boolean; } const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { container: { marginTop: 10, - backgroundColor: theme.centerChannelBg, borderBottomWidth: 1, borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1), }, @@ -44,15 +44,26 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { borderColor: theme.centerChannelBg, marginTop: 2, }, - buttonsContainer: { + header: { marginBottom: 12, paddingHorizontal: 12, flexDirection: 'row', + justifyContent: 'space-between', }, - iconsContainer: { - alignItems: 'center', + buttonContainer: { + flexDirection: 'row', + }, + teamPickerContainer: { + flex: 1, flexDirection: 'row', - marginLeft: 'auto', + justifyContent: 'flex-end', + }, + filterContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + maxWidth: 32, }, }; }); @@ -65,6 +76,7 @@ const Header = ({ selectedTab, selectedFilter, teams, + crossTeamSearchEnabled, }: Props) => { const theme = useTheme(); const styles = getStyleFromTheme(theme); @@ -113,50 +125,50 @@ const Header = ({ theme, title, }); - }, [onFilterChanged, selectedFilter]); - - const filterStyle = useMemo(() => ({marginRight: teams.length > 1 ? 0 : 8}), [teams.length > 1]); + }, [onFilterChanged, selectedFilter, snapPoints, title, theme]); return ( - - - - - {showFilterIcon && ( - - - - - )} + + + + + + {showFilterIcon && ( + + + + + )} + {teams.length > 1 && ( - )} diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index 45386ab6bfc..b5288d532ff 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -18,6 +18,7 @@ import Loading from '@components/loading'; import NavigationHeader from '@components/navigation_header'; import RoundedHeaderContext from '@components/rounded_header_context'; import {Screens} from '@constants'; +import {ALL_TEAMS_ID} from '@constants/team'; import {BOTTOM_TAB_HEIGHT} from '@constants/view'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; @@ -50,6 +51,7 @@ const AutocompletePaddingTop = 4; type Props = { teamId: string; teams: TeamModel[]; + crossTeamSearchEnabled: boolean; } const styles = StyleSheet.create({ @@ -77,7 +79,7 @@ const getSearchParams = (terms: string, filterValue?: FileFilter) => { const searchScreenIndex = 1; -const SearchScreen = ({teamId, teams}: Props) => { +const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { const nav = useNavigation(); const isFocused = useIsFocused(); const intl = useIntl(); @@ -188,7 +190,10 @@ const SearchScreen = ({teamId, teams}: Props) => { hideHeader(true); handleLoading(true); setLastSearchedValue(term); - addSearchToTeamSearchHistory(serverUrl, newSearchTeamId, term); + + if (newSearchTeamId !== ALL_TEAMS_ID) { + addSearchToTeamSearchHistory(serverUrl, newSearchTeamId, term); + } const [postResults, {files, channels}] = await Promise.all([ searchPosts(serverUrl, newSearchTeamId, searchParams), searchFiles(serverUrl, newSearchTeamId, searchParams), @@ -384,6 +389,7 @@ const SearchScreen = ({teamId, teams}: Props) => { selectedTab={selectedTab} selectedFilter={filter} teams={teams} + crossTeamSearchEnabled={crossTeamSearchEnabled} /> } diff --git a/app/screens/home/search/team_picker.test.tsx b/app/screens/home/search/team_picker.test.tsx new file mode 100644 index 00000000000..e6726f11d03 --- /dev/null +++ b/app/screens/home/search/team_picker.test.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {ALL_TEAMS_ID} from '@constants/team'; +import {bottomSheet} from '@screens/navigation'; +import {fireEvent, renderWithIntlAndTheme} from '@test/intl-test-helper'; + +import TeamPicker from './team_picker'; + +import type {TeamModel} from '@database/models/server'; + +jest.mock('@screens/navigation', () => ({ + bottomSheet: jest.fn(), +})); + +// Some subcomponents require react-native-camera-roll, which is not available in the test environment +jest.mock('@react-native-camera-roll/camera-roll', () => ({})); + +describe('TeamPicker', () => { + const teams = [ + {id: 'team1', displayName: 'Team 1'} as TeamModel, + {id: 'team2', displayName: 'Team 2'} as TeamModel, + ]; + + it('should render the selected team name', () => { + const {getByText} = renderWithIntlAndTheme( + , + ); + expect(getByText('Team 1')).toBeTruthy(); + }); + + it('should render "All teams" when teamId is ALL_TEAMS_ID', () => { + const {getByText} = renderWithIntlAndTheme( + , + ); + expect(getByText('All teams')).toBeTruthy(); + }); + + it('should call bottomSheet when the team picker is pressed', () => { + const {getByTestId} = renderWithIntlAndTheme( + , + ); + fireEvent.press(getByTestId('team_picker.button')); + expect(bottomSheet).toHaveBeenCalled(); + }); +}); diff --git a/app/screens/home/search/team_picker_icon.tsx b/app/screens/home/search/team_picker.tsx similarity index 64% rename from app/screens/home/search/team_picker_icon.tsx rename to app/screens/home/search/team_picker.tsx index 954e9043693..4ee98af481d 100644 --- a/app/screens/home/search/team_picker_icon.tsx +++ b/app/screens/home/search/team_picker.tsx @@ -3,12 +3,12 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; +import {Text, View} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {ITEM_HEIGHT} from '@components/slide_up_panel_item'; -import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon'; import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {ALL_TEAMS_ID} from '@constants/team'; import {useTheme} from '@context/theme'; import {TITLE_HEIGHT} from '@screens/bottom_sheet'; import {bottomSheet} from '@screens/navigation'; @@ -25,39 +25,38 @@ const NO_TEAMS_HEIGHT = 392; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { - teamContainer: { - paddingLeft: 8, + teamPicker: { flexDirection: 'row', alignItems: 'center', + justifyContent: 'flex-end', + width: '100%', }, - border: { - marginLeft: 12, - borderLeftWidth: 1, - borderLeftColor: changeOpacity(theme.centerChannelColor, 0.16), + container: { + flex: 1, }, - teamIcon: { - flexDirection: 'row', - }, - compass: { - alignItems: 'center', - marginLeft: 0, + teamName: { + color: theme.centerChannelColor, + fontSize: 12, + textAlign: 'right', }, }; }); type Props = { - size?: number; - divider?: boolean; teams: TeamModel[]; setTeamId: (id: string) => void; teamId: string; + crossTeamSearchEnabled: boolean; } -const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: Props) => { +const TeamPicker = ({setTeamId, teams, teamId, crossTeamSearchEnabled}: Props) => { const intl = useIntl(); const theme = useTheme(); const styles = getStyleFromTheme(theme); - const selectedTeam = teams.find((t) => t.id === teamId); + let selectedTeam = teams.find((t) => t.id === teamId); + if (teamId === ALL_TEAMS_ID) { + selectedTeam = {id: ALL_TEAMS_ID, displayName: intl.formatMessage({id: 'mobile.search.team.all_teams', defaultMessage: 'All teams'})} as TeamModel; + } const title = intl.formatMessage({id: 'mobile.search.team.select', defaultMessage: 'Select a team to search'}); @@ -69,6 +68,7 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: teams={teams} teamId={teamId} title={title} + crossTeamSearchEnabled={crossTeamSearchEnabled} /> ); }; @@ -98,23 +98,18 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: onPress={handleTeamChange} type='opacity' testID='team_picker.button' + style={styles.teamPicker} > - - - - + + {selectedTeam.displayName} + + @@ -124,4 +119,4 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: ); }; -export default TeamPickerIcon; +export default TeamPicker; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 76f2e399ed9..0ec805a5af1 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -774,6 +774,7 @@ "mobile.search.results": "{count} search {count, plural, one {result} other {results}}", "mobile.search.show_less": "Show less", "mobile.search.show_more": "Show more", + "mobile.search.team.all_teams": "All teams", "mobile.search.team.select": "Select a team to search", "mobile.server_identifier.exists": "You are already connected to this server.", "mobile.server_link.error.text": "The link could not be found on this server.", diff --git a/types/api/config.d.ts b/types/api/config.d.ts index 6221eeac8a5..e7b05c3e0be 100644 --- a/types/api/config.d.ts +++ b/types/api/config.d.ts @@ -121,6 +121,7 @@ interface ClientConfig { FeatureFlagCollapsedThreads?: string; FeatureFlagPostPriority?: string; FeatureFlagChannelBookmarks?: string; + FeatureFlagExperimentalCrossTeamSearch?: string; ForgotPasswordLink?: string; GfycatApiKey: string; GfycatApiSecret: string;