Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-60405] Crossteam search #8411

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions app/components/team_list/index.test.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what % of code is being covered with this tests? if 80% or more then OK, if not, let's try and bring it to 80%+ please.

Copy link
Member Author

@JulienTant JulienTant Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be good, jest --coverage app/components/team_list/index.test.tsx --collectCoverageFrom=app/components/team_list/index.tsx gave me

Statements   : 100% ( 16/16 )
Branches     : 91.66% ( 11/12 )
Functions    : 100% ( 5/5 )
Lines        : 100% ( 14/14 )

I had to change the jest.config.js which is set to not report coverage on our components or screens.

Original file line number Diff line number Diff line change
@@ -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(
<TeamList
teams={teams}
onPress={onPress}
/>,
{database},
);
fireEvent.press(getByText('Team 1'));
expect(onPress).toHaveBeenCalledWith('team1');
});

it('should render loading component when loading is true', () => {
const {getByTestId} = renderWithEverything(
<TeamList
teams={teams}
onPress={jest.fn()}
loading={true}
/>,
{database},
);
expect(getByTestId('team_list.loading')).toBeTruthy();
});

it('should render separator after the first item when separatorAfterFirstItem is true', () => {
const {getByTestId} = renderWithEverything(
<TeamList
teams={teams}
onPress={jest.fn()}
separatorAfterFirstItem={true}
/>,
{database},
);
expect(getByTestId('team_list.separator')).toBeTruthy();
});
});
49 changes: 37 additions & 12 deletions app/components/team_list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -47,25 +57,40 @@ 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<Team|TeamModel>) => {
return (
const renderTeam = useCallback(({item: t, index: i}: ListRenderItemInfo<Team|TeamModel>) => {
let teamListItem = (
<TeamListItem
onPress={onPress}
team={t}
textColor={textColor}
iconBackgroundColor={iconBackgroundColor}
iconTextColor={iconTextColor}
selectedTeamId={selectedTeamId}
hideIcon={hideIcon}
/>
);
}, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]);
if (separatorAfterFirstItem && i === 0) {
teamListItem = (<>
{teamListItem}
<View
style={styles.separator}
testID='team_list.separator'
/>
</>);
}
return teamListItem;
}, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId, hideIcon, separatorAfterFirstItem, styles.separator]);

let footer;
if (loading) {
footer = (<Loading/>);
footer = (<Loading testID='team_list.loading'/>);
}

return (
Expand Down
57 changes: 57 additions & 0 deletions app/components/team_list/team_list_item/team_list_item.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<TeamListItem
team={team}
onPress={onPressMock}
/>,
);

fireEvent.press(getByText('Team Display Name'));

expect(onPressMock).toHaveBeenCalledWith('team_id');
});

it('should render TeamIcon when hideIcon is false', () => {
const {getByTestId} = renderWithIntlAndTheme(
<TeamListItem
team={team}
onPress={jest.fn()}
hideIcon={false}
/>,
);

expect(getByTestId(iconTestId)).toBeTruthy();
});

it('should not render TeamIcon when hideIcon is true', () => {
const {queryByTestId} = renderWithIntlAndTheme(
<TeamListItem
team={team}
onPress={jest.fn()}
hideIcon={true}
/>,
);

expect(queryByTestId(iconTestId)).toBeNull();
});
});
27 changes: 15 additions & 12 deletions app/components/team_list/team_list_item/team_list_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Props = {
iconBackgroundColor?: string;
selectedTeamId?: string;
onPress: (teamId: string) => void;
hideIcon?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we hiding the icon? I could not find any reference to this in the tickets nor a figma attached

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new design calls for no icons in the team selector for search, from Figma:

image

}

export const ITEM_HEIGHT = 56;
Expand Down Expand Up @@ -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);

Expand All @@ -68,17 +69,19 @@ export default function TeamListItem({team, textColor, iconTextColor, iconBackgr
type='opacity'
style={styles.touchable}
>
<View style={styles.icon_container}>
<TeamIcon
id={team.id}
displayName={displayName}
lastIconUpdate={lastTeamIconUpdateAt}
selected={false}
textColor={iconTextColor || theme.centerChannelColor}
backgroundColor={iconBackgroundColor || changeOpacity(theme.centerChannelColor, 0.16)}
testID={`${teamListItemTestId}.team_icon`}
/>
</View>
{!hideIcon &&
<View style={styles.icon_container}>
<TeamIcon
id={team.id}
displayName={displayName}
lastIconUpdate={lastTeamIconUpdateAt}
selected={false}
textColor={iconTextColor || theme.centerChannelColor}
backgroundColor={iconBackgroundColor || changeOpacity(theme.centerChannelColor, 0.16)}
testID={`${teamListItemTestId}.team_icon`}
/>
</View>
}
<Text
style={[styles.text, Boolean(textColor) && {color: textColor}]}
numberOfLines={1}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for long names, should we add an ellipsis ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already handled!
image

Expand Down
2 changes: 2 additions & 0 deletions app/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import ServerErrors from './server_errors';
import SnackBar from './snack_bar';
import Sso from './sso';
import SupportedServer from './supported_server';
import Team from './team';
import Tutorial from './tutorial';
import View from './view';
import WebsocketEvents from './websocket';
Expand Down Expand Up @@ -74,6 +75,7 @@ export {
SnackBar,
Sso,
SupportedServer,
Team,
Tutorial,
View,
WebsocketEvents,
Expand Down
8 changes: 8 additions & 0 deletions app/constants/team.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

export const ALL_TEAMS_ID = '';

export default {
ALL_TEAMS_ID,
};
20 changes: 17 additions & 3 deletions app/screens/home/search/bottom_sheet_team_list.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useCallback} from 'react';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';

import TeamList from '@components/team_list';
import {ALL_TEAMS_ID} from '@constants/team';
import {useIsTablet} from '@hooks/device';
import BottomSheetContent from '@screens/bottom_sheet/content';
import {dismissBottomSheet} from '@screens/navigation';
Expand All @@ -15,9 +17,11 @@ type Props = {
teamId: string;
setTeamId: (teamId: string) => 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);

Expand All @@ -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 (
<BottomSheetContent
showButton={false}
Expand All @@ -35,10 +47,12 @@ export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: P
>
<TeamList
selectedTeamId={teamId}
teams={teams}
teams={teamList}
onPress={onPress}
testID='search.select_team_slide_up.team_list'
type={isTablet ? 'FlatList' : 'BottomSheetFlatList'}
hideIcon={true}
separatorAfterFirstItem={crossTeamSearchEnabled}
/>
</BottomSheetContent>
);
Expand Down
3 changes: 2 additions & 1 deletion app/screens/home/search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,6 +16,7 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
return {
teamId: currentTeamId,
teams: queryJoinedTeams(database).observe(),
crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'),
};
});

Expand Down
2 changes: 2 additions & 0 deletions app/screens/home/search/initial/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,6 +23,7 @@ const enhance = withObservables(['teamId'], ({database, teamId}: EnhanceProps) =
switchMap((t) => of$(t?.displayName || '')),
distinctUntilChanged(),
),
crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'),
};
});

Expand Down
Loading