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

Add recent serches and recent chats in Search router #49457

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0acddec
add SingleIconListItem
SzymczakJ Sep 17, 2024
682a8ec
add SearchRouterList
SzymczakJ Sep 17, 2024
386a8f7
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 18, 2024
5ed32cd
add find section to SearchRouterList
SzymczakJ Sep 18, 2024
0b3fe4b
add onSelectRow action
SzymczakJ Sep 19, 2024
3799068
clean up code
SzymczakJ Sep 19, 2024
2f3b0a5
add contextual search
SzymczakJ Sep 20, 2024
80370ca
fix big screen styling
SzymczakJ Sep 20, 2024
50d4e78
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 20, 2024
17cda40
fix pr comments
SzymczakJ Sep 24, 2024
7df4c32
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 24, 2024
48a4296
change contextual search logic
SzymczakJ Sep 25, 2024
96e309a
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 25, 2024
445a031
fix linter
SzymczakJ Sep 25, 2024
6e4d2b2
fix Enter shortcut logic
SzymczakJ Sep 26, 2024
7e509d5
fix SearchRouterList types
SzymczakJ Sep 26, 2024
4e439a2
fix SearchQueryListItem
SzymczakJ Sep 26, 2024
38ba637
fix typescript errors
SzymczakJ Sep 26, 2024
e9cba4e
fix styling
SzymczakJ Sep 26, 2024
cf57d6e
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 26, 2024
ab3109a
fix iOS
SzymczakJ Sep 27, 2024
48510b5
limit recentSearches amountto 5
SzymczakJ Sep 27, 2024
fb0fc7f
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Sep 27, 2024
26e9026
fix PR comments
SzymczakJ Sep 27, 2024
e5ae868
add recent chat filtering logic
SzymczakJ Sep 30, 2024
b5f33da
fix PR coments
SzymczakJ Oct 1, 2024
7b111e8
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Oct 1, 2024
66fa714
fix linter
SzymczakJ Oct 1, 2024
11525fa
fix PR comments
SzymczakJ Oct 1, 2024
994e17f
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Oct 2, 2024
1b7f6ac
fix styles
SzymczakJ Oct 2, 2024
a986ab6
fix searchRouterLoading bug
SzymczakJ Oct 2, 2024
3b9e946
Merge branch 'main' into @szymczak/serach-router-list
SzymczakJ Oct 2, 2024
524eb8b
focus list item if it exists
SzymczakJ Oct 2, 2024
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
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ const ONYXKEYS = {
/** Stores the information about the saved searches */
SAVED_SEARCHES: 'nvp_savedSearches',

/** Stores the information about the recent searches */
RECENT_SEARCHES: 'nvp_recentSearches',

/** Stores recently used currencies */
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',

Expand Down Expand Up @@ -861,6 +864,7 @@ type OnyxValuesMapping = {
// ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data
[ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot;
[ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[];
[ONYXKEYS.RECENT_SEARCHES]: Record<string, OnyxTypes.RecentSearchItem>;
[ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[];
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
Expand Down
87 changes: 76 additions & 11 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
import {useNavigationState} from '@react-navigation/native';
import debounce from 'lodash/debounce';
import React, {useCallback, useState} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
import Modal from '@components/Modal';
import {useOptionsList} from '@components/OptionListContextProvider';
import type {SearchQueryJSON} from '@components/Search/types';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as SearchUtils from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {useSearchRouterContext} from './SearchRouterContext';
import SearchRouterInput from './SearchRouterInput';
import SearchRouterList from './SearchRouterList';

const SEARCH_DEBOUNCE_DELAY = 200;

function SearchRouter() {
const styles = useThemeStyles();

const [betas] = useOnyx(`${ONYXKEYS.BETAS}`);
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
const {isSmallScreenWidth} = useResponsiveLayout();
const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext();

const [currentQuery, setCurrentQuery] = useState<SearchQueryJSON | undefined>(undefined);
const contextualReportID = useNavigationState<Record<string, {reportID: string}>, string | undefined>((state) => {
return state.routes.at(-1)?.params?.reportID;
});
const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES);
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
const sortedRecentSearches = Object.values(recentSearches ?? {}).sort((a, b) => {
const dateA = new Date(a.timestamp);
const dateB = new Date(b.timestamp);
return dateB.getTime() - dateA.getTime();
});
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved

const {options, areOptionsInitialized} = useOptionsList({
shouldInitialize: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
shouldInitialize: true,
shouldInitialize: didScreenTransitionEnd,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was wondering, if I should add it but in the end I don't know why do we even need to pass shouldInitialize. I don't understand why do we need to wait for transitionEnd to initialize some data in context. I also didn't notice any bugs when having this set to true. Maybe you have some more context @rayane-djouah

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This Modal is also outside any ScreenWrapperContext so it does not have didScreenTransitionEnd

});
const searchOptions = useMemo(() => {
if (!areOptionsInitialized) {
return [] as unknown as OptionsListUtils.Options;
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
}
const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []);
return optionList;
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
}, [areOptionsInitialized, betas, options]);

const contextualReportData = searchOptions.recentReports?.find((option) => option.reportID === contextualReportID);
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved

const clearUserQuery = () => {
setCurrentQuery(undefined);
Expand All @@ -45,21 +75,46 @@
}
}, SEARCH_DEBOUNCE_DELAY);

const onSearchSubmit = useCallback(() => {
const closeAndClearRouter = useCallback(() => {
closeSearchRouter();

const query = SearchUtils.buildSearchQueryString(currentQuery);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
clearUserQuery();
}, [currentQuery, closeSearchRouter]);
}, [closeSearchRouter]);

const onSearchSubmit = useCallback(
(query: SearchQueryJSON | undefined) => {
if (!query) {
return;
}
closeSearchRouter();
const queryString = SearchUtils.buildSearchQueryString(query);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString}));
clearUserQuery();
},
[closeSearchRouter],
);

<<<<<<< HEAD

Check failure on line 96 in src/components/Search/SearchRouter/SearchRouter.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Merge conflict marker encountered.
useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
() => {
onSearchSubmit(currentQuery);
},
{
captureOnInputs: true,
shouldBubble: false,
},
);

=======

Check failure on line 108 in src/components/Search/SearchRouter/SearchRouter.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Merge conflict marker encountered.
>>>>>>> main

Check failure on line 109 in src/components/Search/SearchRouter/SearchRouter.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Merge conflict marker encountered.
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => {
closeSearchRouter();
clearUserQuery();
});

const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER;
const isFullWidth = isSmallScreenWidth;
const isFullScreen = isSmallScreenWidth;
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
const modalWidth = isFullScreen ? styles.w100 : {width: variables.popoverWidth};

return (
<Modal
Expand All @@ -70,11 +125,21 @@
onClose={closeSearchRouter}
>
<FocusTrapForModal active={isSearchRouterDisplayed}>
<View style={[styles.flex1, styles.p3]}>
<View style={[styles.flex1, styles.p3, modalWidth, styles.mh100, !isFullScreen && styles.mh85vh]}>
<SearchRouterInput
isFullWidth={isFullWidth}
onChange={onSearchChange}
onSubmit={onSearchSubmit}
onSubmit={() => {
onSearchSubmit(currentQuery);
}}
/>

<SearchRouterList
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
currentSearch={currentQuery}
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
reportForContextualSearch={contextualReportData}
recentSearches={sortedRecentSearches}
recentReports={searchOptions?.recentReports?.slice(0, 5)}
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
onRecentSearchSelect={onSearchSubmit}
closeAndClearRouter={closeAndClearRouter}
/>
</View>
</FocusTrapForModal>
Expand Down
8 changes: 2 additions & 6 deletions src/components/Search/SearchRouter/SearchRouterInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React, {useState} from 'react';
import BaseTextInput from '@components/TextInput/BaseTextInput';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';

type SearchRouterInputProps = {
isFullWidth: boolean;
onChange: (searchTerm: string) => void;
onSubmit: () => void;
};

function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputProps) {
function SearchRouterInput({onChange, onSubmit}: SearchRouterInputProps) {
const styles = useThemeStyles();

const [value, setValue] = useState('');
Expand All @@ -20,15 +18,13 @@ function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputP
onChange(text);
};

const modalWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};

return (
<BaseTextInput
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
autoFocus
textInputContainerStyles={[{borderBottomWidth: 0}, modalWidth]}
textInputContainerStyles={[{borderBottomWidth: 0}, styles.w100]}
inputStyle={[styles.searchInputStyle, styles.searchRouterInputStyle, styles.ph2]}
role={CONST.ROLE.PRESENTATION}
autoCapitalize="none"
Expand Down
125 changes: 125 additions & 0 deletions src/components/Search/SearchRouter/SearchRouterList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, {useCallback} from 'react';
import * as Expensicons from '@components/Icon/Expensicons';
import type {SearchQueryJSON} from '@components/Search/types';
import SelectionList from '@components/SelectionList';
import SingleIconListItem from '@components/SelectionList/Search/SingleIconListItem';
import type {ListItemWithSingleIcon, SingleIconListItemProps} from '@components/SelectionList/Search/SingleIconListItem';
import type {SectionListDataType, UserListItemProps} from '@components/SelectionList/types';
import UserListItem from '@components/SelectionList/UserListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {OptionData} from '@libs/ReportUtils';
import * as SearchUtils from '@libs/SearchUtils';
import * as Report from '@userActions/Report';
import ROUTES from '@src/ROUTES';

type ItemWithQuery = {
query: string;
};

type SearchRouterListProps = {
currentSearch: SearchQueryJSON | undefined;
reportForContextualSearch?: OptionData;
recentSearches: ItemWithQuery[] | undefined;
recentReports: OptionData[];
onRecentSearchSelect: (query: SearchQueryJSON | undefined, shouldAddToRecentSearch?: boolean) => void;
closeAndClearRouter: () => void;
};

function SearchRouterItem(props: UserListItemProps<OptionData> | SingleIconListItemProps<ListItemWithSingleIcon & ItemWithQuery>) {
const styles = useThemeStyles();

if ('item' in props && props.item.reportID) {
return (
<UserListItem
pressableStyle={styles.br2}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(props as UserListItemProps<OptionData>)}
/>
);
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <SingleIconListItem {...(props as SingleIconListItemProps<ListItemWithSingleIcon & ItemWithQuery>)} />;
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
}

function SearchRouterList({currentSearch, reportForContextualSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const sections: Array<SectionListDataType<OptionData | (ListItemWithSingleIcon & ItemWithQuery)>> = [];

if (currentSearch?.inputQuery) {
sections.push({
data: [
{
text: currentSearch?.inputQuery,
singleIcon: Expensicons.MagnifyingGlass,
query: currentSearch?.inputQuery,
itemStyle: styles.activeComponentBG,
keyForList: 'findItem',
},
],
});
}

if (reportForContextualSearch) {
sections.push({
data: [
{
text: `${translate('search.searchIn')}${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
singleIcon: Expensicons.MagnifyingGlass,
// We will change it to different behaviour when Search 2.5 autocomplete will be implemented
query: `in:${reportForContextualSearch.reportID}`,
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
itemStyle: styles.activeComponentBG,
keyForList: 'contextualSearch',
},
],
});
}

const recentSearchesData = recentSearches?.map(({query}) => ({
text: query,
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
singleIcon: Expensicons.History,
query,
keyForList: query,
}));

if (recentSearchesData) {
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
sections.push({title: translate('search.recentSearches'), data: recentSearchesData});
}

const recentReportsWithStyle = recentReports.map((item) => ({...item, pressableStyle: styles.br2}));
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
sections.push({title: translate('search.recentChats'), data: recentReportsWithStyle});

const onSelectRow = useCallback(
(item: OptionData | ItemWithQuery) => {
// This is case for handling selection of "Recent search"
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
if ('query' in item && item?.query) {
const queryJSON = SearchUtils.buildSearchQueryJSON(item?.query);
onRecentSearchSelect(queryJSON, true);
return;
}

// This is case for handling selection of "Recent chat"
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
closeAndClearRouter();
if ('reportID' in item && item?.reportID) {
Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID));
} else if ('login' in item) {
Report.navigateToAndOpenReport(item?.login ? [item.login] : []);
}
},
[closeAndClearRouter, onRecentSearchSelect],
);

return (
<SelectionList<OptionData | (ListItemWithSingleIcon & ItemWithQuery)>
sections={sections}
onSelectRow={onSelectRow}
ListItem={SearchRouterItem}
containerStyle={styles.mh100}
/>
);
}

export default SearchRouterList;
export {SearchRouterItem};
75 changes: 75 additions & 0 deletions src/components/SelectionList/Search/SingleIconListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem} from '@components/SelectionList/types';
import TextWithTooltip from '@components/TextWithTooltip';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type IconAsset from '@src/types/utils/IconAsset';

type ListItemWithSingleIcon = {singleIcon: IconAsset} & ListItem;

type SingleIconListItemProps<TItem extends ListItemWithSingleIcon> = {
item: TItem;
isFocused?: boolean;
showTooltip?: boolean;
onSelectRow: (item: TItem) => void;
onFocus?: () => void;
};

function SingleIconListItem<TItem extends ListItemWithSingleIcon>({item, isFocused, showTooltip, onSelectRow, onFocus}: SingleIconListItemProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();

return (
<BaseListItem
item={item}
pressableStyle={[[styles.singleIconListItemStyle, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, item.cursorStyle]]}
wrapperStyle={[styles.flexRow, styles.flex1, styles.justifyContentBetween, styles.userSelectNone, styles.alignItemsCenter]}
isFocused={isFocused}
onSelectRow={onSelectRow}
keyForList={item.keyForList}
onFocus={onFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
<>
{!!item.singleIcon && (
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
<Icon
src={item.singleIcon}
fill={theme.icon}
additionalStyles={styles.mr2}
small
/>
)}
<View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>
<TextWithTooltip
shouldShowTooltip={showTooltip ?? false}
text={item.text ?? ''}
style={[
styles.optionDisplayName,
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
styles.sidebarLinkTextBold,
styles.pre,
item.alternateText ? styles.mb1 : null,
styles.justifyContentCenter,
]}
/>
{!!item.alternateText && (
<TextWithTooltip
shouldShowTooltip={showTooltip ?? false}
text={item.alternateText}
style={[styles.textLabelSupporting, styles.lh16, styles.pre]}
/>
)}
</View>
{!!item.rightElement && item.rightElement}
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
</>
</BaseListItem>
);
}

SingleIconListItem.displayName = 'SingleIconListItem';

export default SingleIconListItem;
export type {ListItemWithSingleIcon, SingleIconListItemProps};
Loading
Loading