From 0acddecd37b3ff86ce0175a6fa00be9cc838eabe Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 17 Sep 2024 15:36:26 +0200 Subject: [PATCH 01/25] add SingleIconListItem --- .../Search/SingleIconListItem.tsx | 75 +++++++++++++++++++ src/components/SelectionList/types.ts | 8 +- src/components/TextWithTooltip/types.ts | 2 +- src/styles/index.ts | 7 ++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/components/SelectionList/Search/SingleIconListItem.tsx diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SingleIconListItem.tsx new file mode 100644 index 000000000000..e67ca6db39a1 --- /dev/null +++ b/src/components/SelectionList/Search/SingleIconListItem.tsx @@ -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 = { + item: TItem; + isFocused?: boolean; + showTooltip?: boolean; + onSelectRow: (item: TItem) => void; + onFocus?: () => void; +}; + +function SingleIconListItem({item, isFocused, showTooltip, onSelectRow, onFocus}: SingleIconListItemProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + <> + {!!item.singleIcon && ( + + )} + + + {!!item.alternateText && ( + + )} + + {!!item.rightElement && item.rightElement} + + + ); +} + +SingleIconListItem.displayName = 'SingleIconListItem'; + +export default SingleIconListItem; +export type {ListItemWithSingleIcon, SingleIconListItemProps}; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 7bdbb03f2101..ae5101ce8406 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,5 +1,6 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; +import type {SearchRouterItem} from '@components/Search/SearchRouter/SearchRouterList'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; @@ -13,6 +14,7 @@ import type ChatListItem from './ChatListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type ReportListItem from './Search/ReportListItem'; +import type SingleIconListItem from './Search/SingleIconListItem'; import type TransactionListItem from './Search/TransactionListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; @@ -30,7 +32,7 @@ type CommonListItemProps = { isDisabled?: boolean | null; /** Whether this item should show Tooltip */ - showTooltip: boolean; + showTooltip?: boolean; /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ canSelectMultiple?: boolean; @@ -305,7 +307,9 @@ type ValidListItem = | typeof InviteMemberListItem | typeof TransactionListItem | typeof ReportListItem - | typeof ChatListItem; + | typeof ChatListItem + | typeof SingleIconListItem + | typeof SearchRouterItem; type Section = { /** Title of the section */ diff --git a/src/components/TextWithTooltip/types.ts b/src/components/TextWithTooltip/types.ts index 4705e2b69a68..e0211adcdba2 100644 --- a/src/components/TextWithTooltip/types.ts +++ b/src/components/TextWithTooltip/types.ts @@ -5,7 +5,7 @@ type TextWithTooltipProps = { text: string; /** Whether to show the tooltip text */ - shouldShowTooltip: boolean; + shouldShowTooltip?: boolean; /** Additional styles */ style?: StyleProp; diff --git a/src/styles/index.ts b/src/styles/index.ts index 44362ab75ded..752026d1c935 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4650,6 +4650,13 @@ const styles = (theme: ThemeColors) => borderRadius: 8, }, + singleIconListItemStyle: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 16, + }, + selectionListStickyHeader: { backgroundColor: theme.appBG, }, From 682a8ec3422d90896a4dea97d3ef99d5fe3ded18 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 17 Sep 2024 15:38:34 +0200 Subject: [PATCH 02/25] add SearchRouterList --- .../Search/SearchRouter/SearchRouter.tsx | 40 ++++++++++++++- .../Search/SearchRouter/SearchRouterList.tsx | 49 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/components/Search/SearchRouter/SearchRouterList.tsx diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 73a86f95719e..665b40f2b641 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,18 +1,23 @@ 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 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; @@ -76,6 +81,35 @@ function SearchRouter() { const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; const isFullWidth = isSmallScreenWidth; + const mockedRecentSearches = [ + { + name: 'Big agree', + query: '123', + }, + { + name: 'GIF', + query: '123', + }, + { + name: 'Greg', + query: '123', + }, + ]; + + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: true, + }); + + const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); + + const searchOptions = useMemo(() => { + if (!areOptionsInitialized) { + return []; + } + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + return optionList.recentReports.slice(0, 5); + }, [areOptionsInitialized, betas, options]); + return ( + diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx new file mode 100644 index 000000000000..45bc6eeab742 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import * as Expensicons from '@components/Icon/Expensicons'; +import SelectionList from '@components/SelectionList'; +import SingleIconListItem from '@components/SelectionList/Search/SingleIconListItem'; +import type {ListItemWithSingleIcon, SingleIconListItemProps} from '@components/SelectionList/Search/SingleIconListItem'; +import type {UserListItemProps} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import type {OptionData} from '@libs/ReportUtils'; + +type RecentSearchObject = { + name: string; + query: string; +}; + +type SearchRouterListProps = { + recentSearches: RecentSearchObject[]; + recentReports: OptionData[]; +}; + +function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { + if ('item' in props && props.item.reportID) { + // eslint-disable-next-line react/jsx-props-no-spreading + return )} />; + } + // eslint-disable-next-line react/jsx-props-no-spreading + return )} />; +} + +function SearchRouterList({recentSearches, recentReports}: SearchRouterListProps) { + const recentSearchesData = recentSearches.map(({name, query}) => ({ + text: name, + singleIcon: Expensicons.History, + query, + })); + + return ( + + sections={[ + {title: 'Recent searches', data: recentSearchesData}, + {title: 'Recent chats', data: recentReports}, + ]} + onSelectRow={() => {}} + ListItem={SearchRouterItem} + /> + ); +} + +export default SearchRouterList; +export {SearchRouterItem}; From 5ed32cd8e30f8d193ba63f0bd2c50c725ce591ac Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 18 Sep 2024 17:31:26 +0200 Subject: [PATCH 03/25] add find section to SearchRouterList --- src/ONYXKEYS.ts | 4 ++ .../Search/SearchRouter/SearchRouter.tsx | 40 ++++++-------- .../Search/SearchRouter/SearchRouterList.tsx | 54 ++++++++++++++----- src/components/SelectionList/BaseListItem.tsx | 3 +- src/components/SelectionList/UserListItem.tsx | 19 ++++--- src/components/SelectionList/types.ts | 4 ++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/API/parameters/AddRecentSearch.ts | 7 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/Permissions.ts | 2 +- src/libs/actions/Search.ts | 16 ++++++ src/pages/Search/AdvancedSearchFilters.tsx | 2 + src/styles/index.ts | 1 + src/styles/utils/spacing.ts | 4 ++ src/types/onyx/RecentSearch.ts | 13 +++++ src/types/onyx/index.ts | 2 + 18 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 src/libs/API/parameters/AddRecentSearch.ts create mode 100644 src/types/onyx/RecentSearch.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 38affd97c637..eaff0fd0a1bc 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -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', @@ -852,6 +855,7 @@ type OnyxValuesMapping = { // ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data [ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot; [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[]; + [ONYXKEYS.RECENT_SEARCHES]: OnyxTypes.RecentSearchItem[]; [ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[]; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index e4caace967b2..7eb6acf01823 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,15 +3,18 @@ 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 * as Expensicons from '@components/Icon/Expensicons'; import Modal from '@components/Modal'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; +import SingleIconListItem from '@components/SelectionList/Search/SingleIconListItem'; 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 * as SearchActions from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -27,6 +30,7 @@ function SearchRouter() { const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const [currentQuery, setCurrentQuery] = useState(undefined); + const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); const clearUserQuery = () => { setCurrentQuery(undefined); @@ -51,20 +55,19 @@ function SearchRouter() { }, SEARCH_DEBOUNCE_DELAY); const onSearchSubmit = useCallback(() => { + if (!currentQuery) { + return; + } closeSearchRouter(); - - const query = SearchUtils.buildSearchQueryString(currentQuery); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + const queryString = SearchUtils.buildSearchQueryString(currentQuery); + SearchActions.addRecentSearch({queryJSON: currentQuery, previousRecentSearches: recentSearches ?? []}); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); clearUserQuery(); - }, [currentQuery, closeSearchRouter]); + }, [currentQuery, closeSearchRouter, recentSearches]); useKeyboardShortcut( CONST.KEYBOARD_SHORTCUTS.ENTER, () => { - if (!currentQuery) { - return; - } - onSearchSubmit(); }, { @@ -81,21 +84,6 @@ function SearchRouter() { const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; const isFullWidth = isSmallScreenWidth; - const mockedRecentSearches = [ - { - name: 'Big agree', - query: '123', - }, - { - name: 'GIF', - query: '123', - }, - { - name: 'Greg', - query: '123', - }, - ]; - const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: true, }); @@ -119,14 +107,16 @@ function SearchRouter() { onClose={closeSearchRouter} > - + + diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 45bc6eeab742..8c7d53294d35 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -1,46 +1,76 @@ -import React from 'react'; +import React, {useRef} 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 {UserListItemProps} from '@components/SelectionList/types'; +import type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import type {OptionData} from '@libs/ReportUtils'; type RecentSearchObject = { - name: string; query: string; }; type SearchRouterListProps = { + currentSearch: SearchQueryJSON | undefined; recentSearches: RecentSearchObject[]; recentReports: OptionData[]; }; function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { + const styles = useThemeStyles(); + if ('item' in props && props.item.reportID) { - // eslint-disable-next-line react/jsx-props-no-spreading - return )} />; + return ( + )} + /> + ); } // eslint-disable-next-line react/jsx-props-no-spreading return )} />; } -function SearchRouterList({recentSearches, recentReports}: SearchRouterListProps) { - const recentSearchesData = recentSearches.map(({name, query}) => ({ - text: name, +function SearchRouterList({currentSearch, recentSearches, recentReports}: SearchRouterListProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const sections: Array> = []; + + if (currentSearch?.inputQuery) { + sections.push({ + data: [ + { + text: currentSearch?.inputQuery, + singleIcon: Expensicons.MagnifyingGlass, + query: currentSearch?.inputQuery, + itemStyle: styles.activeComponentBG, + keyForList: 'findItem', + }, + ], + }); + } + + const recentSearchesData = recentSearches.map(({query}) => ({ + text: query, singleIcon: Expensicons.History, query, + keyForList: query, })); + sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); + + const recentReportsWithStyle = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + sections.push({title: translate('search.recentChats'), data: recentReportsWithStyle}); return ( - sections={[ - {title: 'Recent searches', data: recentSearchesData}, - {title: 'Recent chats', data: recentReports}, - ]} + sections={sections} onSelectRow={() => {}} ListItem={SearchRouterItem} + containerStyle={styles.mt3} /> ); } diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 31207fdbf1d7..7e11976677c6 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -88,7 +88,8 @@ function BaseListItem({ }} disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} - accessibilityLabel={item.text ?? ''} + // accessibilityDisabled + accessibilityLabel="" role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={[!item.isDisabled && styles.hoveredComponentBG, hoverStyle]} diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index 104990cf479c..be9efcb1aed5 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -47,11 +47,12 @@ function UserListItem({ onSelectRow(item); } }, [item, onCheckboxPress, onSelectRow]); - + // console.log('%%%%%\n', 'onFocus', item); + // console.log('%%%%%\n', 'pressable', pressable); return ( ({ rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} - pressableStyle={pressableStyle} + pressableStyle={[isFocused && styles.sidebarLinkActive, pressableStyle]} FooterComponent={ item.invitedSecondaryLogin ? ( @@ -109,11 +110,13 @@ function UserListItem({ ))} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 3cc258ce0204..572ca2576733 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -166,6 +166,9 @@ type ListItem = { /** The style to override the cursor appearance */ cursorStyle?: CursorStyles[keyof CursorStyles]; + + /** The style to override the default appearance */ + itemStyle?: StyleProp; }; type TransactionListItemType = ListItem & @@ -548,6 +551,7 @@ type BaseSelectionListProps = Partial & { type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; clearInputAfterSelect?: () => void; + focusItem?: (keyForList: string) => void; }; type ItemLayout = { diff --git a/src/languages/en.ts b/src/languages/en.ts index a7ddea880161..4006da23df21 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4065,6 +4065,8 @@ export default { past: 'Past', }, expenseType: 'Expense type', + recentSearches: 'Recent searches', + recentChats: 'Recent chats', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index a8481ec305fc..897b866950d2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4116,6 +4116,8 @@ export default { past: 'Anterior', }, expenseType: 'Tipo de gasto', + recentSearches: 'Búsquedas recientes', + recentChats: 'Chats recientes', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', diff --git a/src/libs/API/parameters/AddRecentSearch.ts b/src/libs/API/parameters/AddRecentSearch.ts new file mode 100644 index 000000000000..2db7e9d05b04 --- /dev/null +++ b/src/libs/API/parameters/AddRecentSearch.ts @@ -0,0 +1,7 @@ +import type {SearchQueryString} from '@components/Search/types'; + +type AddRecentSearchParams = { + jsonQuery: SearchQueryString; +}; + +export default AddRecentSearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 97a5ddadf7c9..e5aeb06ea379 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -306,6 +306,7 @@ export type {default as ToggleCardContinuousReconciliationParams} from './Toggle export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; export type {default as SaveSearchParams} from './SaveSearch'; +export type {default as AddRecentSearch} from './AddRecentSearch'; export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch'; export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams'; export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 5c6102da81d8..ca02573c6d7b 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -377,6 +377,7 @@ const WRITE_COMMANDS = { TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation', SAVE_SEARCH: 'SaveSearch', DELETE_SAVED_SEARCH: 'DeleteSavedSearch', + ADD_RECENT_SEARCH: 'AddRecentSearch', UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency', UPDATE_CARD_SETTLEMENT_ACCOUNT: 'UpdateCardSettlementAccount', UPDATE_XERO_IMPORT_TRACKING_CATEGORIES: 'UpdateXeroImportTrackingCategories', @@ -791,6 +792,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.TOGGLE_CARD_CONTINUOUS_RECONCILIATION]: Parameters.ToggleCardContinuousReconciliationParams; [WRITE_COMMANDS.SAVE_SEARCH]: Parameters.SaveSearchParams; [WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams; + [WRITE_COMMANDS.ADD_RECENT_SEARCH]: Parameters.AddRecentSearch; [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams; [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams; [WRITE_COMMANDS.SET_MISSING_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARD]: Parameters.SetMissingPersonalDetailsAndShipExpensifyCardParams; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 7f7e89ad3585..f0978634b527 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -58,7 +58,7 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry): boolean { * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. */ function canUseNewSearchRouter() { - return Environment.isDevelopment(); + return true; } /** diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a4f0e59ef976..e13fe108d76b 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -12,6 +12,7 @@ import enhanceParameters from '@libs/Network/enhanceParameters'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; +import type {RecentSearchItem} from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; @@ -62,6 +63,20 @@ function deleteSavedSearch(hash: number) { API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}); } +// this is mocked interaction, BE is not ready yet +function addRecentSearch({queryJSON, previousRecentSearches}: {queryJSON: SearchQueryJSON; previousRecentSearches: RecentSearchItem[]}) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.RECENT_SEARCHES}`, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + value: [{query: queryJSON?.inputQuery, timestamp: 'someTimestamp'}, ...previousRecentSearches].slice(0, 5), + }, + ]; + + API.write(WRITE_COMMANDS.ADD_RECENT_SEARCH, {jsonQuery: queryJSON?.inputQuery}, {optimisticData}); +} + function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) { const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash); const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON; @@ -173,4 +188,5 @@ export { clearAdvancedFilters, deleteSavedSearch, dismissSavedSearchRenameTooltip, + addRecentSearch, }; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index ad6416896447..30b3d3c215ad 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -227,11 +227,13 @@ function AdvancedSearchFilters() { const taxRates = getAllTaxRates(); const personalDetails = usePersonalDetails(); const currentType = searchAdvancedFilters?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; + const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); const queryString = useMemo(() => SearchUtils.buildQueryStringFromFilterValues(searchAdvancedFilters) || '', [searchAdvancedFilters]); const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(queryString || SearchUtils.buildCannedSearchQuery()) ?? ({} as SearchQueryJSON), [queryString]); const applyFiltersAndNavigate = () => { + SearchActions.addRecentSearch({queryJSON, previousRecentSearches: recentSearches ?? []}); SearchActions.clearAllFilters(); Navigation.dismissModal(); Navigation.navigate( diff --git a/src/styles/index.ts b/src/styles/index.ts index 62a181d07c86..a7cef55f8c96 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4651,6 +4651,7 @@ const styles = (theme: ThemeColors) => flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 16, + borderRadius: 8, }, selectionListStickyHeader: { diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index 1bf1fbcbbd79..580f1f1867e0 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -15,6 +15,10 @@ export default { margin: 8, }, + m3: { + margin: 12, + }, + m4: { margin: 16, }, diff --git a/src/types/onyx/RecentSearch.ts b/src/types/onyx/RecentSearch.ts new file mode 100644 index 000000000000..738d57956e5c --- /dev/null +++ b/src/types/onyx/RecentSearch.ts @@ -0,0 +1,13 @@ +/** + * Model of a single recent search + */ +type RecentSearchItem = { + /** Query string for the recent search */ + query: string; + + /** Timestamp of recent search */ + timestamp: string; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {RecentSearchItem}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index c7c264a3da15..65beb95907cc 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -64,6 +64,7 @@ import type QuickAction from './QuickAction'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; +import type {RecentSearchItem} from './RecentSearch'; import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type Report from './Report'; @@ -232,6 +233,7 @@ export type { WorkspaceTooltip, CardFeeds, SaveSearch, + RecentSearchItem, ImportedSpreadsheet, ValidateMagicCodeAction, }; From 0b3fe4bba4038c6eb6bf5d976222ab90b5fab862 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 19 Sep 2024 12:57:35 +0200 Subject: [PATCH 04/25] add onSelectRow action --- .../Search/SearchRouter/SearchRouter.tsx | 41 ++++++++++++------- .../Search/SearchRouter/SearchRouterList.tsx | 36 +++++++++++++--- .../Search/TransactionListItemRow.tsx | 12 +++--- src/libs/actions/Search.ts | 3 +- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 7eb6acf01823..bb85aabf393b 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,18 +3,15 @@ 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 * as Expensicons from '@components/Icon/Expensicons'; import Modal from '@components/Modal'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; -import SingleIconListItem from '@components/SelectionList/Search/SingleIconListItem'; 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 * as SearchActions from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -31,6 +28,11 @@ function SearchRouter() { const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const [currentQuery, setCurrentQuery] = useState(undefined); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); + 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(); + }); const clearUserQuery = () => { setCurrentQuery(undefined); @@ -54,21 +56,28 @@ function SearchRouter() { } }, SEARCH_DEBOUNCE_DELAY); - const onSearchSubmit = useCallback(() => { - if (!currentQuery) { - return; - } + const closeAndClearRouter = useCallback(() => { closeSearchRouter(); - const queryString = SearchUtils.buildSearchQueryString(currentQuery); - SearchActions.addRecentSearch({queryJSON: currentQuery, previousRecentSearches: recentSearches ?? []}); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); clearUserQuery(); - }, [currentQuery, closeSearchRouter, recentSearches]); + }, [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], + ); useKeyboardShortcut( CONST.KEYBOARD_SHORTCUTS.ENTER, () => { - onSearchSubmit(); + onSearchSubmit(currentQuery); }, { captureOnInputs: true, @@ -111,13 +120,17 @@ function SearchRouter() { { + onSearchSubmit(currentQuery); + }} /> diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 8c7d53294d35..bd87f9565837 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -1,14 +1,18 @@ -import React, {useRef} from 'react'; +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, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types'; +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 RecentSearchObject = { query: string; @@ -16,8 +20,10 @@ type RecentSearchObject = { type SearchRouterListProps = { currentSearch: SearchQueryJSON | undefined; - recentSearches: RecentSearchObject[]; + recentSearches: RecentSearchObject[] | undefined; recentReports: OptionData[]; + onRecentSearchSelect: (query: SearchQueryJSON | undefined, shouldAddToRecentSearch?: boolean) => void; + closeAndClearRouter: () => void; }; function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { @@ -26,6 +32,7 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI if ('item' in props && props.item.reportID) { return ( )} /> @@ -35,7 +42,7 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI return )} />; } -function SearchRouterList({currentSearch, recentSearches, recentReports}: SearchRouterListProps) { +function SearchRouterList({currentSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const sections: Array> = []; @@ -54,7 +61,7 @@ function SearchRouterList({currentSearch, recentSearches, recentReports}: Search }); } - const recentSearchesData = recentSearches.map(({query}) => ({ + const recentSearchesData = recentSearches?.map(({query}) => ({ text: query, singleIcon: Expensicons.History, query, @@ -65,10 +72,27 @@ function SearchRouterList({currentSearch, recentSearches, recentReports}: Search const recentReportsWithStyle = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); sections.push({title: translate('search.recentChats'), data: recentReportsWithStyle}); + const onSelectRow = useCallback( + (item: OptionData | ListItemWithSingleIcon) => { + if (item?.query) { + const queryJSON = SearchUtils.buildSearchQueryJSON(item?.query); + onRecentSearchSelect(queryJSON, true); + return; + } + closeAndClearRouter(); + if ((item as OptionData)?.reportID) { + Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute((item as OptionData)?.reportID)); + } else { + Report.navigateToAndOpenReport((item as OptionData)?.login ? [(item as OptionData)?.login] : []); + } + }, + [closeAndClearRouter, onRecentSearchSelect], + ); + return ( sections={sections} - onSelectRow={() => {}} + onSelectRow={onSelectRow} ListItem={SearchRouterItem} containerStyle={styles.mt3} /> diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 4a59b58a8273..1ac3484e6619 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -81,10 +81,10 @@ function ReceiptCell({transactionItem}: TransactionCellProps) { const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); - const filename = getFileName(transactionItem?.receipt?.source ?? ''); - const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); - const isReceiptPDF = Str.isPDF(filename); - const source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); + // const filename = getFileName(transactionItem?.receipt?.source ?? ''); + // const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); + // const isReceiptPDF = Str.isPDF(filename); + // const source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); return ( - + /> */} ); } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index e13fe108d76b..a693cb0b6a23 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -65,12 +65,13 @@ function deleteSavedSearch(hash: number) { // this is mocked interaction, BE is not ready yet function addRecentSearch({queryJSON, previousRecentSearches}: {queryJSON: SearchQueryJSON; previousRecentSearches: RecentSearchItem[]}) { + const uniqueRecentSearches = previousRecentSearches.filter((recentSearch) => recentSearch.query === queryJSON.inputQuery); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.RECENT_SEARCHES}`, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - value: [{query: queryJSON?.inputQuery, timestamp: 'someTimestamp'}, ...previousRecentSearches].slice(0, 5), + value: [{query: queryJSON?.inputQuery, timestamp: 'someTimestamp'}, ...uniqueRecentSearches].slice(0, 5), }, ]; From 37990688b515e0d234f7d25ac86775c45b1c0cea Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 19 Sep 2024 13:26:29 +0200 Subject: [PATCH 05/25] clean up code --- .../Search/SearchRouter/SearchRouterList.tsx | 32 +++++++++++-------- src/components/SelectionList/BaseListItem.tsx | 3 +- .../Search/TransactionListItemRow.tsx | 12 +++---- src/components/SelectionList/UserListItem.tsx | 14 ++++---- src/components/SelectionList/types.ts | 1 - src/libs/API/parameters/AddRecentSearch.ts | 7 ---- src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 -- src/libs/Permissions.ts | 2 +- src/libs/actions/Search.ts | 17 ---------- 10 files changed, 33 insertions(+), 58 deletions(-) delete mode 100644 src/libs/API/parameters/AddRecentSearch.ts diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index bd87f9565837..e7069dafdbd2 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -14,19 +14,19 @@ import * as SearchUtils from '@libs/SearchUtils'; import * as Report from '@userActions/Report'; import ROUTES from '@src/ROUTES'; -type RecentSearchObject = { +type ItemWithQuery = { query: string; }; type SearchRouterListProps = { currentSearch: SearchQueryJSON | undefined; - recentSearches: RecentSearchObject[] | undefined; + recentSearches: ItemWithQuery[] | undefined; recentReports: OptionData[]; onRecentSearchSelect: (query: SearchQueryJSON | undefined, shouldAddToRecentSearch?: boolean) => void; closeAndClearRouter: () => void; }; -function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { +function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { const styles = useThemeStyles(); if ('item' in props && props.item.reportID) { @@ -39,13 +39,13 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI ); } // eslint-disable-next-line react/jsx-props-no-spreading - return )} />; + return )} />; } function SearchRouterList({currentSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const sections: Array> = []; + const sections: Array> = []; if (currentSearch?.inputQuery) { sections.push({ @@ -67,30 +67,36 @@ function SearchRouterList({currentSearch, recentSearches, recentReports, onRecen query, keyForList: query, })); - sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); + + if (recentSearchesData) { + sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); + } const recentReportsWithStyle = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); sections.push({title: translate('search.recentChats'), data: recentReportsWithStyle}); const onSelectRow = useCallback( - (item: OptionData | ListItemWithSingleIcon) => { - if (item?.query) { + (item: OptionData | ItemWithQuery) => { + // This is case for handling selection of "Recent search" + 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" closeAndClearRouter(); - if ((item as OptionData)?.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute((item as OptionData)?.reportID)); - } else { - Report.navigateToAndOpenReport((item as OptionData)?.login ? [(item as OptionData)?.login] : []); + 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 ( - + sections={sections} onSelectRow={onSelectRow} ListItem={SearchRouterItem} diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 7e11976677c6..31207fdbf1d7 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -88,8 +88,7 @@ function BaseListItem({ }} disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} - // accessibilityDisabled - accessibilityLabel="" + accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={[!item.isDisabled && styles.hoveredComponentBG, hoverStyle]} diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 1ac3484e6619..4a59b58a8273 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -81,10 +81,10 @@ function ReceiptCell({transactionItem}: TransactionCellProps) { const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); - // const filename = getFileName(transactionItem?.receipt?.source ?? ''); - // const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); - // const isReceiptPDF = Str.isPDF(filename); - // const source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); + const filename = getFileName(transactionItem?.receipt?.source ?? ''); + const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); + const isReceiptPDF = Str.isPDF(filename); + const source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); return ( - {/* */} + /> ); } diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index be9efcb1aed5..451e5409d29d 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -52,7 +52,7 @@ function UserListItem({ return ( ({ ))} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 572ca2576733..5b14af4060de 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -551,7 +551,6 @@ type BaseSelectionListProps = Partial & { type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; clearInputAfterSelect?: () => void; - focusItem?: (keyForList: string) => void; }; type ItemLayout = { diff --git a/src/libs/API/parameters/AddRecentSearch.ts b/src/libs/API/parameters/AddRecentSearch.ts deleted file mode 100644 index 2db7e9d05b04..000000000000 --- a/src/libs/API/parameters/AddRecentSearch.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {SearchQueryString} from '@components/Search/types'; - -type AddRecentSearchParams = { - jsonQuery: SearchQueryString; -}; - -export default AddRecentSearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e5aeb06ea379..97a5ddadf7c9 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -306,7 +306,6 @@ export type {default as ToggleCardContinuousReconciliationParams} from './Toggle export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; export type {default as SaveSearchParams} from './SaveSearch'; -export type {default as AddRecentSearch} from './AddRecentSearch'; export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch'; export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams'; export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ca02573c6d7b..5c6102da81d8 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -377,7 +377,6 @@ const WRITE_COMMANDS = { TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation', SAVE_SEARCH: 'SaveSearch', DELETE_SAVED_SEARCH: 'DeleteSavedSearch', - ADD_RECENT_SEARCH: 'AddRecentSearch', UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency', UPDATE_CARD_SETTLEMENT_ACCOUNT: 'UpdateCardSettlementAccount', UPDATE_XERO_IMPORT_TRACKING_CATEGORIES: 'UpdateXeroImportTrackingCategories', @@ -792,7 +791,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.TOGGLE_CARD_CONTINUOUS_RECONCILIATION]: Parameters.ToggleCardContinuousReconciliationParams; [WRITE_COMMANDS.SAVE_SEARCH]: Parameters.SaveSearchParams; [WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams; - [WRITE_COMMANDS.ADD_RECENT_SEARCH]: Parameters.AddRecentSearch; [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams; [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams; [WRITE_COMMANDS.SET_MISSING_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARD]: Parameters.SetMissingPersonalDetailsAndShipExpensifyCardParams; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index f0978634b527..7f7e89ad3585 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -58,7 +58,7 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry): boolean { * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. */ function canUseNewSearchRouter() { - return true; + return Environment.isDevelopment(); } /** diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a693cb0b6a23..a4f0e59ef976 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -12,7 +12,6 @@ import enhanceParameters from '@libs/Network/enhanceParameters'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; -import type {RecentSearchItem} from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; @@ -63,21 +62,6 @@ function deleteSavedSearch(hash: number) { API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}); } -// this is mocked interaction, BE is not ready yet -function addRecentSearch({queryJSON, previousRecentSearches}: {queryJSON: SearchQueryJSON; previousRecentSearches: RecentSearchItem[]}) { - const uniqueRecentSearches = previousRecentSearches.filter((recentSearch) => recentSearch.query === queryJSON.inputQuery); - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.RECENT_SEARCHES}`, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - value: [{query: queryJSON?.inputQuery, timestamp: 'someTimestamp'}, ...uniqueRecentSearches].slice(0, 5), - }, - ]; - - API.write(WRITE_COMMANDS.ADD_RECENT_SEARCH, {jsonQuery: queryJSON?.inputQuery}, {optimisticData}); -} - function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) { const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash); const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON; @@ -189,5 +173,4 @@ export { clearAdvancedFilters, deleteSavedSearch, dismissSavedSearchRenameTooltip, - addRecentSearch, }; From 2f3b0a52a62a7f3887d31779fdfad33a55d38708 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 20 Sep 2024 16:41:54 +0200 Subject: [PATCH 06/25] add contextual search --- .../Search/SearchRouter/SearchRouter.tsx | 37 +++++++++++-------- .../Search/SearchRouter/SearchRouterList.tsx | 17 ++++++++- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index bb85aabf393b..4b258b874855 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import debounce from 'lodash/debounce'; import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -23,10 +24,14 @@ const SEARCH_DEBOUNCE_DELAY = 200; function SearchRouter() { const styles = useThemeStyles(); - + const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); + const [currentQuery, setCurrentQuery] = useState(undefined); + const contextualReportID = useNavigationState, string | undefined>((state) => { + return state.routes.at(-1)?.params?.reportID; + }); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); const sortedRecentSearches = Object.values(recentSearches ?? {}).sort((a, b) => { const dateA = new Date(a.timestamp); @@ -34,6 +39,19 @@ function SearchRouter() { return dateB.getTime() - dateA.getTime(); }); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: true, + }); + const searchOptions = useMemo(() => { + if (!areOptionsInitialized) { + return [] as unknown as OptionsListUtils.Options; + } + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + return optionList; + }, [areOptionsInitialized, betas, options]); + + const contextualReportData = searchOptions.recentReports?.find((option) => option.reportID === contextualReportID); + const clearUserQuery = () => { setCurrentQuery(undefined); }; @@ -93,20 +111,6 @@ function SearchRouter() { const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; const isFullWidth = isSmallScreenWidth; - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: true, - }); - - const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); - - const searchOptions = useMemo(() => { - if (!areOptionsInitialized) { - return []; - } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); - return optionList.recentReports.slice(0, 5); - }, [areOptionsInitialized, betas, options]); - return ( diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index e7069dafdbd2..f9446e8c51bc 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -20,6 +20,7 @@ type ItemWithQuery = { type SearchRouterListProps = { currentSearch: SearchQueryJSON | undefined; + reportForContextualSearch?: OptionData; recentSearches: ItemWithQuery[] | undefined; recentReports: OptionData[]; onRecentSearchSelect: (query: SearchQueryJSON | undefined, shouldAddToRecentSearch?: boolean) => void; @@ -42,7 +43,7 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI return )} />; } -function SearchRouterList({currentSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { +function SearchRouterList({currentSearch, reportForContextualSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const sections: Array> = []; @@ -61,6 +62,20 @@ function SearchRouterList({currentSearch, recentSearches, recentReports, onRecen }); } + if (reportForContextualSearch) { + sections.push({ + data: [ + { + text: `${translate('search.searchIn')}${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, + singleIcon: Expensicons.MagnifyingGlass, + query: `in:${reportForContextualSearch.reportID}`, + itemStyle: styles.activeComponentBG, + keyForList: 'contextualSearch', + }, + ], + }); + } + const recentSearchesData = recentSearches?.map(({query}) => ({ text: query, singleIcon: Expensicons.History, diff --git a/src/languages/en.ts b/src/languages/en.ts index 4006da23df21..0c1c581f2711 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4067,6 +4067,7 @@ export default { expenseType: 'Expense type', recentSearches: 'Recent searches', recentChats: 'Recent chats', + searchIn: 'Search in ', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 897b866950d2..2bfffc2a659d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4118,6 +4118,7 @@ export default { expenseType: 'Tipo de gasto', recentSearches: 'Búsquedas recientes', recentChats: 'Chats recientes', + searchIn: 'Buscar en ', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', From 80370ca913a8a73b621c6d67b9cf81ce9a2defb2 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 20 Sep 2024 17:32:39 +0200 Subject: [PATCH 07/25] fix big screen styling --- src/ONYXKEYS.ts | 2 +- src/components/Search/SearchRouter/SearchRouter.tsx | 7 ++++--- src/components/Search/SearchRouter/SearchRouterInput.tsx | 8 ++------ src/components/Search/SearchRouter/SearchRouterList.tsx | 3 ++- src/components/SelectionList/UserListItem.tsx | 3 +-- src/pages/Search/AdvancedSearchFilters.tsx | 2 -- src/styles/utils/sizing.ts | 4 ++++ 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index eaff0fd0a1bc..8487a5b998a4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -855,7 +855,7 @@ type OnyxValuesMapping = { // ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data [ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot; [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[]; - [ONYXKEYS.RECENT_SEARCHES]: OnyxTypes.RecentSearchItem[]; + [ONYXKEYS.RECENT_SEARCHES]: Record; [ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[]; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 4b258b874855..33849a6bb0c1 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -13,6 +13,7 @@ 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'; @@ -109,7 +110,8 @@ function SearchRouter() { }); const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; - const isFullWidth = isSmallScreenWidth; + const isFullScreen = isSmallScreenWidth; + const modalWidth = isFullScreen ? styles.w100 : {width: variables.popoverWidth}; return ( - + { onSearchSubmit(currentQuery); diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 860a46239d21..b1405f2c9701 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -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(''); @@ -20,15 +18,13 @@ function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputP onChange(text); }; - const modalWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; - return ( ); } diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index 451e5409d29d..57c46db18af2 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -47,8 +47,7 @@ function UserListItem({ onSelectRow(item); } }, [item, onCheckboxPress, onSelectRow]); - // console.log('%%%%%\n', 'onFocus', item); - // console.log('%%%%%\n', 'pressable', pressable); + return ( SearchUtils.buildQueryStringFromFilterValues(searchAdvancedFilters) || '', [searchAdvancedFilters]); const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(queryString || SearchUtils.buildCannedSearchQuery()) ?? ({} as SearchQueryJSON), [queryString]); const applyFiltersAndNavigate = () => { - SearchActions.addRecentSearch({queryJSON, previousRecentSearches: recentSearches ?? []}); SearchActions.clearAllFilters(); Navigation.dismissModal(); Navigation.navigate( diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts index f4be70391eb5..ed4651bcf2e0 100644 --- a/src/styles/utils/sizing.ts +++ b/src/styles/utils/sizing.ts @@ -37,6 +37,10 @@ export default { maxHeight: '100%', }, + mh85vh: { + maxHeight: '85vh', + }, + mnh100: { minHeight: '100%', }, From 17cda40acc6d236ad07bcbfc44bcd3f68a89df6e Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 24 Sep 2024 17:05:32 +0200 Subject: [PATCH 08/25] fix pr comments --- .../Search/SearchRouter/SearchRouter.tsx | 43 +++++++------------ .../Search/SearchRouter/SearchRouterList.tsx | 21 +++++---- .../Search/SingleIconListItem.tsx | 5 +-- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index d35c9b80de36..70d2637ffc08 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -26,6 +26,8 @@ const SEARCH_DEBOUNCE_DELAY = 200; function SearchRouter() { const styles = useThemeStyles(); const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); + const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); + const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); @@ -33,25 +35,25 @@ function SearchRouter() { const contextualReportID = useNavigationState, string | undefined>((state) => { return state.routes.at(-1)?.params?.reportID; }); - const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); - 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(); - }); + const sortedRecentSearches = useMemo(() => { + return Object.values(recentSearches ?? {}).sort((a, b) => { + const dateA = new Date(a.timestamp); + const dateB = new Date(b.timestamp); + return dateB.getTime() - dateA.getTime(); + }); + }, [recentSearches]); const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: true, }); const searchOptions = useMemo(() => { if (!areOptionsInitialized) { - return [] as unknown as OptionsListUtils.Options; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); - return optionList; + return OptionsListUtils.getSearchOptions(options, '', betas ?? []); }, [areOptionsInitialized, betas, options]); - const contextualReportData = searchOptions.recentReports?.find((option) => option.reportID === contextualReportID); + const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { setCurrentQuery(undefined); @@ -93,28 +95,13 @@ function SearchRouter() { [closeSearchRouter], ); -<<<<<<< HEAD - useKeyboardShortcut( - CONST.KEYBOARD_SHORTCUTS.ENTER, - () => { - onSearchSubmit(currentQuery); - }, - { - captureOnInputs: true, - shouldBubble: false, - }, - ); - -======= ->>>>>>> main useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { closeSearchRouter(); clearUserQuery(); }); const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; - const isFullScreen = isSmallScreenWidth; - const modalWidth = isFullScreen ? styles.w100 : {width: variables.popoverWidth}; + const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - + { @@ -134,7 +121,7 @@ function SearchRouter() { /> | SingleIconListI if ('item' in props && props.item.reportID) { return ( )} /> @@ -43,18 +42,18 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI return )} />; } -function SearchRouterList({currentSearch, reportForContextualSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { +function SearchRouterList({currentQuery, reportForContextualSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const sections: Array> = []; - if (currentSearch?.inputQuery) { + if (currentQuery?.inputQuery) { sections.push({ data: [ { - text: currentSearch?.inputQuery, + text: currentQuery?.inputQuery, singleIcon: Expensicons.MagnifyingGlass, - query: currentSearch?.inputQuery, + query: currentQuery?.inputQuery, itemStyle: styles.activeComponentBG, keyForList: 'findItem', }, @@ -84,23 +83,23 @@ function SearchRouterList({currentSearch, reportForContextualSearch, recentSearc keyForList: query, })); - if (recentSearchesData) { + if (recentSearchesData && recentSearchesData.length > 0) { sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const recentReportsWithStyle = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); - sections.push({title: translate('search.recentChats'), data: recentReportsWithStyle}); + const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + sections.push({title: translate('search.recentChats'), data: styledRecentReports}); const onSelectRow = useCallback( (item: OptionData | ItemWithQuery) => { - // This is case for handling selection of "Recent search" + // Handle selection of "Recent search" 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" + // Handle selection of "Recent chat" closeAndClearRouter(); if ('reportID' in item && item?.reportID) { Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SingleIconListItem.tsx index e67ca6db39a1..784aa4822335 100644 --- a/src/components/SelectionList/Search/SingleIconListItem.tsx +++ b/src/components/SelectionList/Search/SingleIconListItem.tsx @@ -34,7 +34,7 @@ function SingleIconListItem({item, isFocus hoverStyle={item.isSelected && styles.activeComponentBG} > <> - {!!item.singleIcon && ( + {item.singleIcon && ( ({item, isFocus styles.justifyContentCenter, ]} /> - {!!item.alternateText && ( + {item.alternateText && ( ({item, isFocus /> )} - {!!item.rightElement && item.rightElement} ); From 48a429681a46579e619bca83f84bc5beda510750 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 25 Sep 2024 08:04:45 +0200 Subject: [PATCH 09/25] change contextual search logic --- src/CONST.ts | 5 ++ .../Search/SearchRouter/SearchRouter.tsx | 45 +++++++----- .../Search/SearchRouter/SearchRouterInput.tsx | 14 ++-- .../Search/SearchRouter/SearchRouterList.tsx | 69 ++++++++++++------- src/components/Search/types.ts | 7 ++ .../Search/SingleIconListItem.tsx | 2 +- 6 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9ee9ec4d9147..0fc0b67565ce 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5578,6 +5578,11 @@ const CONST = { KEYWORD: 'keyword', IN: 'in', }, + ROUTER_LIST_ITEM_TYPE: { + REPORT: 'report', + SEARCH: 'search', + CONTEXTUAL_SUGGESTION: 'contextualSuggestion', + }, }, REFERRER: { diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index d73c3e8265c2..94b5c75a626c 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -31,6 +31,7 @@ function SearchRouter() { const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); + const [textInputValue, setTextInputValue] = useState(''); const [userSearchQuery, setUserSearchQuery] = useState(undefined); const contextualReportID = useNavigationState, string | undefined>((state) => { return state.routes.at(-1)?.params?.reportID; @@ -56,26 +57,35 @@ function SearchRouter() { const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { + setTextInputValue(''); setUserSearchQuery(undefined); }; - const onSearchChange = debounce((userQuery: string) => { - if (!userQuery) { - clearUserQuery(); - return; - } + const onSearchChange = useCallback( + debounce((userQuery: string) => { + if (!userQuery) { + clearUserQuery(); + return; + } - const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); + const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); - if (queryJSON) { - // eslint-disable-next-line - console.log('parsedQuery', queryJSON); + if (queryJSON) { + // eslint-disable-next-line + console.log('parsedQuery', queryJSON); - setUserSearchQuery(queryJSON); - } else { - // Handle query parsing error - } - }, SEARCH_DEBOUNCE_DELAY); + setUserSearchQuery(queryJSON); + } else { + // Handle query parsing error + } + }, SEARCH_DEBOUNCE_DELAY), + [], + ); + + const updateUserSearchQuery = (newSearchQuery: string) => { + setTextInputValue(newSearchQuery); + onSearchChange(newSearchQuery); + }; const closeAndClearRouter = useCallback(() => { closeSearchRouter(); @@ -114,7 +124,9 @@ function SearchRouter() { { onSearchSubmit(userSearchQuery); }} @@ -125,7 +137,8 @@ function SearchRouter() { reportForContextualSearch={contextualReportData} recentSearches={sortedRecentSearches} recentReports={searchOptions?.recentReports?.slice(0, 5)} - onRecentSearchSelect={onSearchSubmit} + onSearchSubmit={onSearchSubmit} + updateUserSearchQuery={updateUserSearchQuery} closeAndClearRouter={closeAndClearRouter} /> diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index b1405f2c9701..1a067d22f1d9 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -4,23 +4,23 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type SearchRouterInputProps = { - onChange: (searchTerm: string) => void; + text: string; + setText: (searchTerm: string) => void; + updateSearch: (searchTerm: string) => void; onSubmit: () => void; }; -function SearchRouterInput({onChange, onSubmit}: SearchRouterInputProps) { +function SearchRouterInput({text, setText, updateSearch, onSubmit}: SearchRouterInputProps) { const styles = useThemeStyles(); - const [value, setValue] = useState(''); - const onChangeText = (text: string) => { - setValue(text); - onChange(text); + setText(text); + updateSearch(text); }; return ( void; + onSearchSubmit: (query: SearchQueryJSON | undefined) => void; + updateUserSearchQuery: (newSearchQuery: string) => void; closeAndClearRouter: () => void; }; -function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { +function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { const styles = useThemeStyles(); - - if ('item' in props && props.item.reportID) { + if (props.item.itemType === CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT) { return ( )} /> @@ -42,10 +44,10 @@ function SearchRouterItem(props: UserListItemProps | SingleIconListI return )} />; } -function SearchRouterList({currentQuery, reportForContextualSearch, recentSearches, recentReports, onRecentSearchSelect, closeAndClearRouter}: SearchRouterListProps) { +function SearchRouterList({currentQuery, reportForContextualSearch, recentSearches, recentReports, onSearchSubmit, updateUserSearchQuery, closeAndClearRouter}: SearchRouterListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const sections: Array> = []; + const sections: Array> = []; if (currentQuery?.inputQuery) { sections.push({ @@ -56,6 +58,7 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch query: currentQuery?.inputQuery, itemStyle: styles.activeComponentBG, keyForList: 'findItem', + itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH, }, ], }); @@ -67,10 +70,10 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch { text: `${translate('search.searchIn')}${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, singleIcon: Expensicons.MagnifyingGlass, - // We will change it to different behaviour when Search 2.5 autocomplete will be implemented query: `in:${reportForContextualSearch.reportID}`, itemStyle: styles.activeComponentBG, keyForList: 'contextualSearch', + itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.CONTEXTUAL_SUGGESTION, }, ], }); @@ -81,37 +84,50 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch singleIcon: Expensicons.History, query, keyForList: query, + itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH, })); if (recentSearchesData && recentSearchesData.length > 0) { sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); - sections.push({title: translate('search.recentChats'), data: styledRecentReports}); + const recentReportsData = recentReports.map((item) => ({...item, pressableStyle: styles.br2, itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT})); + sections.push({title: translate('search.recentChats'), data: recentReportsData}); const onSelectRow = useCallback( - (item: OptionData | ItemWithQuery) => { - // Handle selection of "Recent search" - if ('query' in item && item?.query) { - const queryJSON = SearchUtils.buildSearchQueryJSON(item?.query); - onRecentSearchSelect(queryJSON, true); - return; - } - - // Handle selection of "Recent chat" - 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] : []); + (item: SearchRouterListItem) => { + switch (item.itemType) { + case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH: + // Handle selection of "Recent search" + if (!('query' in item) || !item?.query) { + return; + } + const queryJSON = SearchUtils.buildSearchQueryJSON(item?.query); + onSearchSubmit(queryJSON); + return; + case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.CONTEXTUAL_SUGGESTION: + // Handle selection of "Contextual search suggestion" + if (!('query' in item) || !item?.query || currentQuery?.inputQuery.includes(item?.query)) { + return; + } + updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`); + return; + case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT: + // Handle selection of "Recent chat" + 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] : []); + } + return; } }, - [closeAndClearRouter, onRecentSearchSelect], + [closeAndClearRouter, onSearchSubmit, currentQuery], ); return ( - + sections={sections} onSelectRow={onSelectRow} ListItem={SearchRouterItem} @@ -122,3 +138,4 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch export default SearchRouterList; export {SearchRouterItem}; +export type {ItemWithQuery}; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 195073f8b89f..c76f8959533e 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,6 +1,10 @@ import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; +import type {ListItemWithSingleIcon, SingleIconListItemProps} from '@components/SelectionList/Search/SingleIconListItem'; +import type {ListItemProps, UserListItemProps} from '@components/SelectionList/types'; +import type {OptionData} from '@libs/ReportUtils'; import type CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import type {ItemWithQuery} from './SearchRouter/SearchRouterList'; /** Model of the selected transaction */ type SelectedTransactionInfo = { @@ -73,6 +77,8 @@ type SearchQueryJSON = { flatFilters: QueryFilters; } & SearchQueryAST; +type SearchRouterListItem = (OptionData | (ListItemWithSingleIcon & ItemWithQuery)) & {itemType?: ValueOf}; + export type { SelectedTransactionInfo, SelectedTransactions, @@ -91,4 +97,5 @@ export type { InvoiceSearchStatus, TripSearchStatus, ChatSearchStatus, + SearchRouterListItem, }; diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SingleIconListItem.tsx index 784aa4822335..9aa3274d3686 100644 --- a/src/components/SelectionList/Search/SingleIconListItem.tsx +++ b/src/components/SelectionList/Search/SingleIconListItem.tsx @@ -8,7 +8,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type IconAsset from '@src/types/utils/IconAsset'; -type ListItemWithSingleIcon = {singleIcon: IconAsset} & ListItem; +type ListItemWithSingleIcon = {singleIcon?: IconAsset} & ListItem; type SingleIconListItemProps = { item: TItem; From 445a0315caed56f37560bbd5f514d40ecb679237 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 25 Sep 2024 08:53:47 +0200 Subject: [PATCH 10/25] fix linter --- .../Search/SearchRouter/SearchRouter.tsx | 35 +++++++++---------- .../Search/SearchRouter/SearchRouterInput.tsx | 12 +++---- .../Search/SearchRouter/SearchRouterList.tsx | 7 ++-- src/components/Search/types.ts | 3 +- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 94b5c75a626c..da7998a265e9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -61,26 +61,23 @@ function SearchRouter() { setUserSearchQuery(undefined); }; - const onSearchChange = useCallback( - debounce((userQuery: string) => { - if (!userQuery) { - clearUserQuery(); - return; - } + const onSearchChange = debounce((userQuery: string) => { + if (!userQuery) { + clearUserQuery(); + return; + } - const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); + const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); - if (queryJSON) { - // eslint-disable-next-line - console.log('parsedQuery', queryJSON); + if (queryJSON) { + // eslint-disable-next-line + console.log('parsedQuery', queryJSON); - setUserSearchQuery(queryJSON); - } else { - // Handle query parsing error - } - }, SEARCH_DEBOUNCE_DELAY), - [], - ); + setUserSearchQuery(queryJSON); + } else { + // Handle query parsing error + } + }, SEARCH_DEBOUNCE_DELAY); const updateUserSearchQuery = (newSearchQuery: string) => { setTextInputValue(newSearchQuery); @@ -124,8 +121,8 @@ function SearchRouter() { { onSearchSubmit(userSearchQuery); diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 1a067d22f1d9..ee5924002a06 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -1,26 +1,26 @@ -import React, {useState} from 'react'; +import React from 'react'; import BaseTextInput from '@components/TextInput/BaseTextInput'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type SearchRouterInputProps = { - text: string; - setText: (searchTerm: string) => void; + value: string; + setValue: (searchTerm: string) => void; updateSearch: (searchTerm: string) => void; onSubmit: () => void; }; -function SearchRouterInput({text, setText, updateSearch, onSubmit}: SearchRouterInputProps) { +function SearchRouterInput({value, setValue, updateSearch, onSubmit}: SearchRouterInputProps) { const styles = useThemeStyles(); const onChangeText = (text: string) => { - setText(text); + setValue(text); updateSearch(text); }; return ( { + // eslint-disable-next-line default-case switch (item.itemType) { case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH: // Handle selection of "Recent search" if (!('query' in item) || !item?.query) { return; } - const queryJSON = SearchUtils.buildSearchQueryJSON(item?.query); - onSearchSubmit(queryJSON); + onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query)); return; case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.CONTEXTUAL_SUGGESTION: // Handle selection of "Contextual search suggestion" @@ -120,10 +120,9 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch } else if ('login' in item) { Report.navigateToAndOpenReport(item?.login ? [item.login] : []); } - return; } }, - [closeAndClearRouter, onSearchSubmit, currentQuery], + [closeAndClearRouter, onSearchSubmit, currentQuery, updateUserSearchQuery], ); return ( diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index c76f8959533e..360d8e8fe902 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,6 +1,5 @@ import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; -import type {ListItemWithSingleIcon, SingleIconListItemProps} from '@components/SelectionList/Search/SingleIconListItem'; -import type {ListItemProps, UserListItemProps} from '@components/SelectionList/types'; +import type {ListItemWithSingleIcon} from '@components/SelectionList/Search/SingleIconListItem'; import type {OptionData} from '@libs/ReportUtils'; import type CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; From 6e4d2b27431f4ce83f7608dcd4dea0252b7bf5a3 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 26 Sep 2024 12:14:19 +0200 Subject: [PATCH 11/25] fix Enter shortcut logic --- .../Search/SearchRouter/SearchRouter.tsx | 9 ++++++--- .../Search/SearchRouter/SearchRouterInput.tsx | 11 ++++++++++- .../Search/SearchRouter/SearchRouterList.tsx | 15 ++++++++++----- .../SelectionList/BaseSelectionList.tsx | 16 +++++++++++++++- .../SelectionList/Search/SingleIconListItem.tsx | 4 +++- src/components/SelectionList/types.ts | 2 ++ 6 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index da7998a265e9..2e9a02912c2b 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,12 +1,13 @@ import {useNavigationState} from '@react-navigation/native'; import debounce from 'lodash/debounce'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useRef, 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 type {SelectionListHandle} from '@components/SelectionList/types'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,6 +31,7 @@ function SearchRouter() { const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); + const listRef = useRef(null); const [textInputValue, setTextInputValue] = useState(''); const [userSearchQuery, setUserSearchQuery] = useState(undefined); @@ -66,7 +68,7 @@ function SearchRouter() { clearUserQuery(); return; } - + listRef.current?.updateAndScrollToFocusedIndex(0); const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); if (queryJSON) { @@ -127,8 +129,8 @@ function SearchRouter() { onSubmit={() => { onSearchSubmit(userSearchQuery); }} + routerListRef={listRef} /> - diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index ee5924002a06..6ad5d4df0ba7 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import type {RefObject} from 'react'; +import type {SelectionListHandle} from '@components/SelectionList/types'; import BaseTextInput from '@components/TextInput/BaseTextInput'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -8,9 +10,10 @@ type SearchRouterInputProps = { setValue: (searchTerm: string) => void; updateSearch: (searchTerm: string) => void; onSubmit: () => void; + routerListRef: RefObject; }; -function SearchRouterInput({value, setValue, updateSearch, onSubmit}: SearchRouterInputProps) { +function SearchRouterInput({value, setValue, updateSearch, onSubmit, routerListRef}: SearchRouterInputProps) { const styles = useThemeStyles(); const onChangeText = (text: string) => { @@ -28,6 +31,12 @@ function SearchRouterInput({value, setValue, updateSearch, onSubmit}: SearchRout inputStyle={[styles.searchInputStyle, styles.searchRouterInputStyle, styles.ph2]} role={CONST.ROLE.PRESENTATION} autoCapitalize="none" + onFocus={() => { + routerListRef?.current?.updateExternalTextInputFocus(true); + }} + onBlur={() => { + routerListRef?.current?.updateExternalTextInputFocus(false); + }} /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index b8a01a92d349..4550829d3476 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -1,10 +1,11 @@ -import React, {useCallback} from 'react'; +import React, {forwardRef, useCallback} from 'react'; +import type {ForwardedRef} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import type {SearchQueryJSON, SearchRouterListItem} 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 type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -44,7 +45,10 @@ function SearchRouterItem(props: UserListItemProps | Singl return )} />; } -function SearchRouterList({currentQuery, reportForContextualSearch, recentSearches, recentReports, onSearchSubmit, updateUserSearchQuery, closeAndClearRouter}: SearchRouterListProps) { +function SearchRouterList( + {currentQuery, reportForContextualSearch, recentSearches, recentReports, onSearchSubmit, updateUserSearchQuery, closeAndClearRouter}: SearchRouterListProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const {translate} = useLocalize(); const sections: Array> = []; @@ -96,7 +100,7 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch const onSelectRow = useCallback( (item: SearchRouterListItem) => { - // eslint-disable-next-line default-case + // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check switch (item.itemType) { case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH: // Handle selection of "Recent search" @@ -131,10 +135,11 @@ function SearchRouterList({currentQuery, reportForContextualSearch, recentSearch onSelectRow={onSelectRow} ListItem={SearchRouterItem} containerStyle={styles.mh100} + ref={ref} /> ); } -export default SearchRouterList; +export default forwardRef(SearchRouterList); export {SearchRouterItem}; export type {ItemWithQuery}; diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 197c64b99a26..fb594d2f1d6b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -617,7 +617,21 @@ function BaseSelectionList( [flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex], ); - useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect}), [scrollAndHighlightItem, clearInputAfterSelect]); + /** + * Changes isTextInputFocusedRef value when using external TextInput, to handle shouldSync focus properly + * + * @param isTextInputFocused - Is external TextInput focused. + */ + const updateExternalTextInputFocus = useCallback((isTextInputFocused: boolean) => { + isTextInputFocusedRef.current = isTextInputFocused; + }, []); + + useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus}), [ + scrollAndHighlightItem, + clearInputAfterSelect, + updateAndScrollToFocusedIndex, + updateExternalTextInputFocus, + ]); /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SingleIconListItem.tsx index 9aa3274d3686..73f6e0ebd4c6 100644 --- a/src/components/SelectionList/Search/SingleIconListItem.tsx +++ b/src/components/SelectionList/Search/SingleIconListItem.tsx @@ -16,9 +16,10 @@ type SingleIconListItemProps = { showTooltip?: boolean; onSelectRow: (item: TItem) => void; onFocus?: () => void; + shouldSyncFocus?: boolean; }; -function SingleIconListItem({item, isFocused, showTooltip, onSelectRow, onFocus}: SingleIconListItemProps) { +function SingleIconListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SingleIconListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -32,6 +33,7 @@ function SingleIconListItem({item, isFocus keyForList={item.keyForList} onFocus={onFocus} hoverStyle={item.isSelected && styles.activeComponentBG} + shouldSyncFocus={shouldSyncFocus} > <> {item.singleIcon && ( diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8c8c33eb2cc9..395e1f27397f 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -554,6 +554,8 @@ type BaseSelectionListProps = Partial & { type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; clearInputAfterSelect?: () => void; + updateAndScrollToFocusedIndex: (newFocusedIndex: number) => void; + updateExternalTextInputFocus: (isTextInputFocused: boolean) => void; }; type ItemLayout = { From 7e509d5bcff72891ca4152208ab4f86e6b4dbc25 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 26 Sep 2024 16:16:32 +0200 Subject: [PATCH 12/25] fix SearchRouterList types --- .../Search/SearchRouter/SearchRouterList.tsx | 84 ++++++++++--------- src/components/Search/types.ts | 6 -- ...onListItem.tsx => SearchQueryListItem.tsx} | 16 ++-- src/components/SelectionList/types.ts | 4 +- 4 files changed, 56 insertions(+), 54 deletions(-) rename src/components/SelectionList/Search/{SingleIconListItem.tsx => SearchQueryListItem.tsx} (84%) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 4550829d3476..6cf99a00c8f3 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -1,10 +1,10 @@ import React, {forwardRef, useCallback} from 'react'; import type {ForwardedRef} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; -import type {SearchQueryJSON, SearchRouterListItem} from '@components/Search/types'; +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 SearchQueryListItem from '@components/SelectionList/Search/SearchQueryListItem'; +import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem'; import type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; @@ -30,19 +30,31 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; -function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { +function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps { + if ('singleIcon' in listItem.item && listItem.item.singleIcon && 'query' in listItem.item && !!listItem.item.query) { + return true; + } + return false; +} + +function SearchRouterItem(props: UserListItemProps | SearchQueryListItemProps) { const styles = useThemeStyles(); - if (props.item.itemType === CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT) { + + if (isSearchQueryListItem(props)) { return ( - )} + {...props} /> ); } - // eslint-disable-next-line react/jsx-props-no-spreading - return )} />; + return ( + + ); } function SearchRouterList( @@ -51,7 +63,7 @@ function SearchRouterList( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const sections: Array> = []; + const sections: Array> = []; if (currentQuery?.inputQuery) { sections.push({ @@ -62,7 +74,6 @@ function SearchRouterList( query: currentQuery?.inputQuery, itemStyle: styles.activeComponentBG, keyForList: 'findItem', - itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH, }, ], }); @@ -77,7 +88,7 @@ function SearchRouterList( query: `in:${reportForContextualSearch.reportID}`, itemStyle: styles.activeComponentBG, keyForList: 'contextualSearch', - itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.CONTEXTUAL_SUGGESTION, + isContextualSearchItem: true, }, ], }); @@ -88,7 +99,6 @@ function SearchRouterList( singleIcon: Expensicons.History, query, keyForList: query, - itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH, })); if (recentSearchesData && recentSearchesData.length > 0) { @@ -99,38 +109,32 @@ function SearchRouterList( sections.push({title: translate('search.recentChats'), data: recentReportsData}); const onSelectRow = useCallback( - (item: SearchRouterListItem) => { - // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check - switch (item.itemType) { - case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.SEARCH: - // Handle selection of "Recent search" - if (!('query' in item) || !item?.query) { - return; - } - onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query)); - return; - case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.CONTEXTUAL_SUGGESTION: - // Handle selection of "Contextual search suggestion" - if (!('query' in item) || !item?.query || currentQuery?.inputQuery.includes(item?.query)) { - return; - } - updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`); - return; - case CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT: - // Handle selection of "Recent chat" - 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] : []); - } + (item: OptionData | SearchQueryItem) => { + if (!('query' in item) || !item.query) { + // Handle selection of "Recent chat" + 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] : []); + } + return; } + + if (item.isContextualSearchItem) { + // Handle selection of "Contextual search suggestion" + updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`); + return; + } + + // Handle selection of "Recent search" + onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query)); }, [closeAndClearRouter, onSearchSubmit, currentQuery, updateUserSearchQuery], ); return ( - + sections={sections} onSelectRow={onSelectRow} ListItem={SearchRouterItem} diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 360d8e8fe902..195073f8b89f 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,9 +1,6 @@ import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; -import type {ListItemWithSingleIcon} from '@components/SelectionList/Search/SingleIconListItem'; -import type {OptionData} from '@libs/ReportUtils'; import type CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; -import type {ItemWithQuery} from './SearchRouter/SearchRouterList'; /** Model of the selected transaction */ type SelectedTransactionInfo = { @@ -76,8 +73,6 @@ type SearchQueryJSON = { flatFilters: QueryFilters; } & SearchQueryAST; -type SearchRouterListItem = (OptionData | (ListItemWithSingleIcon & ItemWithQuery)) & {itemType?: ValueOf}; - export type { SelectedTransactionInfo, SelectedTransactions, @@ -96,5 +91,4 @@ export type { InvoiceSearchStatus, TripSearchStatus, ChatSearchStatus, - SearchRouterListItem, }; diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx similarity index 84% rename from src/components/SelectionList/Search/SingleIconListItem.tsx rename to src/components/SelectionList/Search/SearchQueryListItem.tsx index 73f6e0ebd4c6..69290aad337a 100644 --- a/src/components/SelectionList/Search/SingleIconListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -8,9 +8,13 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type IconAsset from '@src/types/utils/IconAsset'; -type ListItemWithSingleIcon = {singleIcon?: IconAsset} & ListItem; +type SearchQueryItem = { + singleIcon: IconAsset; + query: string; + isContextualSearchItem?: boolean; +} & ListItem; -type SingleIconListItemProps = { +type SearchQueryListItemProps = { item: TItem; isFocused?: boolean; showTooltip?: boolean; @@ -19,7 +23,7 @@ type SingleIconListItemProps = { shouldSyncFocus?: boolean; }; -function SingleIconListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SingleIconListItemProps) { +function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SearchQueryListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -70,7 +74,7 @@ function SingleIconListItem({item, isFocus ); } -SingleIconListItem.displayName = 'SingleIconListItem'; +SearchQueryListItem.displayName = 'SingleIconListItem'; -export default SingleIconListItem; -export type {ListItemWithSingleIcon, SingleIconListItemProps}; +export default SearchQueryListItem; +export type {SearchQueryItem, SearchQueryListItemProps}; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 395e1f27397f..e17d2497398e 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -14,7 +14,7 @@ import type ChatListItem from './ChatListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type ReportListItem from './Search/ReportListItem'; -import type SingleIconListItem from './Search/SingleIconListItem'; +import type SearchQueryListItem from './Search/SearchQueryListItem'; import type TransactionListItem from './Search/TransactionListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; @@ -315,7 +315,7 @@ type ValidListItem = | typeof TransactionListItem | typeof ReportListItem | typeof ChatListItem - | typeof SingleIconListItem + | typeof SearchQueryListItem | typeof SearchRouterItem; type Section = { From 4e439a2b0dfbe8a4b87384a591387055221bcaee Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 26 Sep 2024 17:05:32 +0200 Subject: [PATCH 13/25] fix SearchQueryListItem --- .../Search/SearchRouter/SearchRouterList.tsx | 9 ++++----- .../Search/SearchQueryListItem.tsx | 17 +++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 6cf99a00c8f3..9fbdeedddabf 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -13,7 +13,6 @@ 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 CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; type ItemWithQuery = { @@ -30,14 +29,14 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; -function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps { +function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps { if ('singleIcon' in listItem.item && listItem.item.singleIcon && 'query' in listItem.item && !!listItem.item.query) { return true; } return false; } -function SearchRouterItem(props: UserListItemProps | SearchQueryListItemProps) { +function SearchRouterItem(props: UserListItemProps | SearchQueryListItemProps) { const styles = useThemeStyles(); if (isSearchQueryListItem(props)) { @@ -105,8 +104,8 @@ function SearchRouterList( sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const recentReportsData = recentReports.map((item) => ({...item, pressableStyle: styles.br2, itemType: CONST.SEARCH.ROUTER_LIST_ITEM_TYPE.REPORT})); - sections.push({title: translate('search.recentChats'), data: recentReportsData}); + const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + sections.push({title: translate('search.recentChats'), data: styledRecentReports}); const onSelectRow = useCallback( (item: OptionData | SearchQueryItem) => { diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index 69290aad337a..0145b1331c59 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -8,22 +8,23 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type IconAsset from '@src/types/utils/IconAsset'; -type SearchQueryItem = { - singleIcon: IconAsset; - query: string; +type SearchQueryItem = ListItem & { + singleIcon?: IconAsset; + query?: string; isContextualSearchItem?: boolean; -} & ListItem; +}; -type SearchQueryListItemProps = { - item: TItem; +type SearchQueryListItemProps = { + item: SearchQueryItem; isFocused?: boolean; showTooltip?: boolean; - onSelectRow: (item: TItem) => void; + onSelectRow: (item: SearchQueryItem) => void; onFocus?: () => void; shouldSyncFocus?: boolean; }; -function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SearchQueryListItemProps) { +// type Testt = SearchQueryItem extends ListItem ? string : boolean; +function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SearchQueryListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); From 38ba637ac9f3ec0e132ec5e04c3b295eed00deeb Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 26 Sep 2024 17:19:51 +0200 Subject: [PATCH 14/25] fix typescript errors --- src/CONST.ts | 5 ----- src/components/SelectionList/BaseSelectionList.tsx | 2 +- src/components/SelectionList/Search/SearchQueryListItem.tsx | 4 ++-- src/components/SelectionList/types.ts | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0fc0b67565ce..9ee9ec4d9147 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5578,11 +5578,6 @@ const CONST = { KEYWORD: 'keyword', IN: 'in', }, - ROUTER_LIST_ITEM_TYPE: { - REPORT: 'report', - SEARCH: 'search', - CONTEXTUAL_SUGGESTION: 'contextualSuggestion', - }, }, REFERRER: { diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index fb594d2f1d6b..cfb9fd23d658 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -618,7 +618,7 @@ function BaseSelectionList( ); /** - * Changes isTextInputFocusedRef value when using external TextInput, to handle shouldSync focus properly + * Handles isTextInputFocusedRef value when using external TextInput, so external TextInput is not defocused when typing in it. * * @param isTextInputFocused - Is external TextInput focused. */ diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index 0145b1331c59..c6e0c4f7b06e 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -17,13 +17,12 @@ type SearchQueryItem = ListItem & { type SearchQueryListItemProps = { item: SearchQueryItem; isFocused?: boolean; - showTooltip?: boolean; + showTooltip: boolean; onSelectRow: (item: SearchQueryItem) => void; onFocus?: () => void; shouldSyncFocus?: boolean; }; -// type Testt = SearchQueryItem extends ListItem ? string : boolean; function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SearchQueryListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -39,6 +38,7 @@ function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus onFocus={onFocus} hoverStyle={item.isSelected && styles.activeComponentBG} shouldSyncFocus={shouldSyncFocus} + showTooltip={showTooltip} > <> {item.singleIcon && ( diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e17d2497398e..e06e6db8a18b 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -32,7 +32,7 @@ type CommonListItemProps = { isDisabled?: boolean | null; /** Whether this item should show Tooltip */ - showTooltip?: boolean; + showTooltip: boolean; /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ canSelectMultiple?: boolean; From e9cba4e8966bc2d4c7c5e917f7818ede56cd74a4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 26 Sep 2024 17:53:25 +0200 Subject: [PATCH 15/25] fix styling --- .../Search/SearchRouter/SearchRouter.tsx | 39 +++++++++++-------- .../Search/SearchRouter/SearchRouterList.tsx | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 2e9a02912c2b..8a3d3528dcd4 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -63,23 +63,28 @@ function SearchRouter() { setUserSearchQuery(undefined); }; - const onSearchChange = debounce((userQuery: string) => { - if (!userQuery) { - clearUserQuery(); - return; - } - listRef.current?.updateAndScrollToFocusedIndex(0); - const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); - - if (queryJSON) { - // eslint-disable-next-line - console.log('parsedQuery', queryJSON); - - setUserSearchQuery(queryJSON); - } else { - // Handle query parsing error - } - }, SEARCH_DEBOUNCE_DELAY); + const onSearchChange = useMemo( + // eslint-disable-next-line react-compiler/react-compiler + () => + debounce((userQuery: string) => { + if (!userQuery) { + clearUserQuery(); + return; + } + listRef.current?.updateAndScrollToFocusedIndex(0); + const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); + + if (queryJSON) { + // eslint-disable-next-line + console.log('parsedQuery', queryJSON); + + setUserSearchQuery(queryJSON); + } else { + // Handle query parsing error + } + }, SEARCH_DEBOUNCE_DELAY), + [], + ); const updateUserSearchQuery = (newSearchQuery: string) => { setTextInputValue(newSearchQuery); diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 9fbdeedddabf..8a12d6df1216 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -137,7 +137,7 @@ function SearchRouterList( sections={sections} onSelectRow={onSelectRow} ListItem={SearchRouterItem} - containerStyle={styles.mh100} + containerStyle={[styles.mh100, styles.mt2]} ref={ref} /> ); From ab3109aec40cd99373d867fa5ecbb4cc64333fa7 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 27 Sep 2024 10:33:27 +0200 Subject: [PATCH 16/25] fix iOS --- .../Search/SearchRouter/SearchRouter.tsx | 16 +++++++++++++--- .../Search/SearchRouter/SearchRouterInput.tsx | 1 + .../Search/SearchRouter/SearchRouterList.tsx | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 8a3d3528dcd4..018045daadce 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -4,11 +4,13 @@ import React, {useCallback, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; import type {SelectionListHandle} from '@components/SelectionList/types'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -26,6 +28,7 @@ const SEARCH_DEBOUNCE_DELAY = 150; function SearchRouter() { const styles = useThemeStyles(); + const {translate} = useLocalize(); const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); @@ -36,7 +39,7 @@ function SearchRouter() { const [textInputValue, setTextInputValue] = useState(''); const [userSearchQuery, setUserSearchQuery] = useState(undefined); const contextualReportID = useNavigationState, string | undefined>((state) => { - return state.routes.at(-1)?.params?.reportID; + return state?.routes.at(-1)?.params?.reportID; }); const sortedRecentSearches = useMemo(() => { return Object.values(recentSearches ?? {}).sort((a, b) => { @@ -69,6 +72,7 @@ function SearchRouter() { debounce((userQuery: string) => { if (!userQuery) { clearUserQuery(); + listRef.current?.updateAndScrollToFocusedIndex(-1); return; } listRef.current?.updateAndScrollToFocusedIndex(0); @@ -114,7 +118,7 @@ function SearchRouter() { clearUserQuery(); }); - const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; + const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE : CONST.MODAL.MODAL_TYPE.POPOVER; const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; return ( @@ -126,7 +130,13 @@ function SearchRouter() { onClose={closeSearchRouter} > - + + {isSmallScreenWidth && ( + closeSearchRouter()} + /> + )} ); From 48510b5c67cac2bed41ca8601fe6065205ad7c14 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 27 Sep 2024 11:10:27 +0200 Subject: [PATCH 17/25] limit recentSearches amountto 5 --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 018045daadce..efd112b74e12 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -149,7 +149,7 @@ function SearchRouter() { Date: Fri, 27 Sep 2024 15:46:01 +0200 Subject: [PATCH 18/25] fix PR comments --- .../Search/SearchRouter/SearchRouter.tsx | 14 ++----- .../Search/SearchRouter/SearchRouterList.tsx | 40 +++++++++++-------- .../Search/SearchQueryListItem.tsx | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index efd112b74e12..3efe1094d27c 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -13,6 +13,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import Log from '@libs/Log'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; @@ -42,11 +43,7 @@ function SearchRouter() { return state?.routes.at(-1)?.params?.reportID; }); const sortedRecentSearches = useMemo(() => { - return Object.values(recentSearches ?? {}).sort((a, b) => { - const dateA = new Date(a.timestamp); - const dateB = new Date(b.timestamp); - return dateB.getTime() - dateA.getTime(); - }); + return Object.values(recentSearches ?? {}).sort((a, b) => b.timestamp.localeCompare(a.timestamp)); }, [recentSearches]); const {options, areOptionsInitialized} = useOptionsList({ @@ -79,12 +76,9 @@ function SearchRouter() { const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); if (queryJSON) { - // eslint-disable-next-line - console.log('parsedQuery', queryJSON); - setUserSearchQuery(queryJSON); } else { - // Handle query parsing error + Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, userQuery, false); } }, SEARCH_DEBOUNCE_DELAY), [], @@ -150,7 +144,7 @@ function SearchRouter() { currentQuery={userSearchQuery} reportForContextualSearch={contextualReportData} recentSearches={sortedRecentSearches?.slice(0, 5)} - recentReports={searchOptions?.recentReports?.slice(0, 5)} + recentReports={searchOptions?.recentReports?.slice(0, 10)} onSearchSubmit={onSearchSubmit} updateUserSearchQuery={updateUserSearchQuery} closeAndClearRouter={closeAndClearRouter} diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 6df0f208f133..ac5e8f11d0c9 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -29,13 +29,17 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; -function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps { - if ('singleIcon' in listItem.item && listItem.item.singleIcon && 'query' in listItem.item && !!listItem.item.query) { +function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { + if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; } return false; } +function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps { + return isSearchQueryItem(listItem.item); +} + function SearchRouterItem(props: UserListItemProps | SearchQueryListItemProps) { const styles = useThemeStyles(); @@ -109,25 +113,27 @@ function SearchRouterList( const onSelectRow = useCallback( (item: OptionData | SearchQueryItem) => { - if (!('query' in item) || !item.query) { - // Handle selection of "Recent chat" - 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] : []); + if (isSearchQueryItem(item)) { + if (item.isContextualSearchItem) { + // Handle selection of "Contextual search suggestion" + updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`); + return; } - return; - } - if (item.isContextualSearchItem) { - // Handle selection of "Contextual search suggestion" - updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`); - return; + // Handle selection of "Recent search" + if (!item?.query) { + return; + } + onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query)); } - // Handle selection of "Recent search" - onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query)); + // Handle selection of "Recent chat" + 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, onSearchSubmit, currentQuery, updateUserSearchQuery], ); diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index c6e0c4f7b06e..6b32b31aae12 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -75,7 +75,7 @@ function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus ); } -SearchQueryListItem.displayName = 'SingleIconListItem'; +SearchQueryListItem.displayName = 'SearchQueryListItem'; export default SearchQueryListItem; export type {SearchQueryItem, SearchQueryListItemProps}; From e5ae86866f0ce8bba5e01621365cbd81d00407ce Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 30 Sep 2024 14:24:50 +0200 Subject: [PATCH 19/25] add recent chat filtering logic --- .../Search/SearchRouter/SearchRouter.tsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 3efe1094d27c..73d8cee77b29 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,6 +1,6 @@ import {useNavigationState} from '@react-navigation/native'; import debounce from 'lodash/debounce'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -9,15 +9,18 @@ import Modal from '@components/Modal'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; import type {SelectionListHandle} from '@components/SelectionList/types'; +import useDebouncedState from '@hooks/useDebouncedState'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionData} from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; +import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -37,7 +40,7 @@ function SearchRouter() { const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const listRef = useRef(null); - const [textInputValue, setTextInputValue] = useState(''); + const [textInputValue, debouncedInputValue, setTextInputValue] = useDebouncedState('', 500); const [userSearchQuery, setUserSearchQuery] = useState(undefined); const contextualReportID = useNavigationState, string | undefined>((state) => { return state?.routes.at(-1)?.params?.reportID; @@ -56,6 +59,37 @@ function SearchRouter() { return OptionsListUtils.getSearchOptions(options, '', betas ?? []); }, [areOptionsInitialized, betas, options]); + const filteredOptions = useMemo(() => { + if (debouncedInputValue.trim() === '') { + return { + recentReports: [], + personalDetails: [], + userToInvite: null, + }; + } + + const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); + + return { + recentReports: newOptions.recentReports, + personalDetails: newOptions.personalDetails, + userToInvite: newOptions.userToInvite, + }; + }, [debouncedInputValue, searchOptions]); + + const recentReports: OptionData[] = useMemo(() => { + const currentSearchOptions = debouncedInputValue === '' ? searchOptions : filteredOptions; + const reports: OptionData[] = [...currentSearchOptions.recentReports, ...currentSearchOptions.personalDetails]; + if (currentSearchOptions.userToInvite) { + reports.push(currentSearchOptions.userToInvite); + } + return reports.slice(0, 10); + }, [debouncedInputValue, filteredOptions, searchOptions]); + + useEffect(() => { + Report.searchInServer(debouncedInputValue.trim()); + }, [debouncedInputValue]); + const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { @@ -92,6 +126,8 @@ function SearchRouter() { const closeAndClearRouter = useCallback(() => { closeSearchRouter(); clearUserQuery(); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps }, [closeSearchRouter]); const onSearchSubmit = useCallback( @@ -104,6 +140,8 @@ function SearchRouter() { Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); clearUserQuery(); }, + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps [closeSearchRouter], ); @@ -144,7 +182,7 @@ function SearchRouter() { currentQuery={userSearchQuery} reportForContextualSearch={contextualReportData} recentSearches={sortedRecentSearches?.slice(0, 5)} - recentReports={searchOptions?.recentReports?.slice(0, 10)} + recentReports={recentReports} onSearchSubmit={onSearchSubmit} updateUserSearchQuery={updateUserSearchQuery} closeAndClearRouter={closeAndClearRouter} From b5f33da6420448ae8b985e410edaedfaf07476dc Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 1 Oct 2024 14:10:53 +0200 Subject: [PATCH 20/25] fix PR coments --- .../Search/SearchRouter/SearchRouter.tsx | 5 +- .../Search/SearchRouter/SearchRouterInput.tsx | 27 ++++++++--- .../Search/SearchRouter/SearchRouterList.tsx | 48 +++++++++++++++---- .../Search/SearchQueryListItem.tsx | 4 +- .../TextInput/BaseTextInput/index.tsx | 3 +- .../TextInput/BaseTextInput/types.ts | 3 ++ src/languages/en.ts | 3 +- src/languages/es.ts | 3 +- src/styles/index.ts | 6 +-- 9 files changed, 74 insertions(+), 28 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 73d8cee77b29..facd60866c69 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -33,7 +33,7 @@ const SEARCH_DEBOUNCE_DELAY = 150; function SearchRouter() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); + const [betas] = useOnyx(ONYXKEYS.BETAS); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); const {isSmallScreenWidth} = useResponsiveLayout(); @@ -173,9 +173,6 @@ function SearchRouter() { value={textInputValue} setValue={setTextInputValue} updateSearch={onSearchChange} - onSubmit={() => { - onSearchSubmit(userSearchQuery); - }} routerListRef={listRef} /> void; + + /** Callback to update search in SearchRouter */ updateSearch: (searchTerm: string) => void; - onSubmit: () => void; + + /** SearchRouterList ref for managing TextInput and SearchRouterList focus */ routerListRef: RefObject; }; -function SearchRouterInput({value, setValue, updateSearch, onSubmit, routerListRef}: SearchRouterInputProps) { +function SearchRouterInput({value, setValue, updateSearch, routerListRef}: SearchRouterInputProps) { const styles = useThemeStyles(); + const {isSmallScreenWidth} = useResponsiveLayout(); + const {translate} = useLocalize(); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const onChangeText = (text: string) => { setValue(text); @@ -25,12 +38,13 @@ function SearchRouterInput({value, setValue, updateSearch, onSubmit, routerListR { routerListRef?.current?.updateExternalTextInputFocus(true); @@ -38,6 +52,7 @@ function SearchRouterInput({value, setValue, updateSearch, onSubmit, routerListR onBlur={() => { routerListRef?.current?.updateExternalTextInputFocus(false); }} + isLoading={!!isSearchingForReports} /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index ac5e8f11d0c9..210fe2b6e91c 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -1,6 +1,8 @@ import React, {forwardRef, useCallback} from 'react'; import type {ForwardedRef} from 'react'; +import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; +import {usePersonalDetails} from '@components/OnyxProvider'; import type {SearchQueryJSON} from '@components/Search/types'; import SelectionList from '@components/SelectionList'; import SearchQueryListItem from '@components/SelectionList/Search/SearchQueryListItem'; @@ -8,11 +10,14 @@ import type {SearchQueryItem, SearchQueryListItemProps} from '@components/Select import type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import * as Report from '@userActions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; type ItemWithQuery = { @@ -20,12 +25,25 @@ type ItemWithQuery = { }; type SearchRouterListProps = { + /** currentQuery value computed coming from parsed TextInput value */ currentQuery: SearchQueryJSON | undefined; - reportForContextualSearch?: OptionData; + + /** Recent searches */ recentSearches: ItemWithQuery[] | undefined; + + /** Recent reports */ recentReports: OptionData[]; + + /** Callback to submit query when selecting a list item */ onSearchSubmit: (query: SearchQueryJSON | undefined) => void; + + /** Context present when opening SearchRouter from a report, invoice or workspace page */ + reportForContextualSearch?: OptionData; + + /** Callback to update search query when selecting contextual suggestion */ updateUserSearchQuery: (newSearchQuery: string) => void; + + /** Callback to close and clear SearchRouter */ closeAndClearRouter: () => void; }; @@ -66,6 +84,13 @@ function SearchRouterList( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {isSmallScreenWidth} = useResponsiveLayout(); + + const personalDetails = usePersonalDetails(); + const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const taxRates = getAllTaxRates(); + const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST); + const contextualQuery = `in:${reportForContextualSearch?.reportID}`; const sections: Array> = []; if (currentQuery?.inputQuery) { @@ -82,11 +107,11 @@ function SearchRouterList( }); } - if (reportForContextualSearch) { + if (reportForContextualSearch && !currentQuery?.inputQuery?.includes(contextualQuery)) { sections.push({ data: [ { - text: `${translate('search.searchIn')}${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, + text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, singleIcon: Expensicons.MagnifyingGlass, query: `in:${reportForContextualSearch.reportID}`, itemStyle: styles.activeComponentBG, @@ -97,12 +122,15 @@ function SearchRouterList( }); } - const recentSearchesData = recentSearches?.map(({query}) => ({ - text: query, - singleIcon: Expensicons.History, - query, - keyForList: query, - })); + const recentSearchesData = recentSearches?.map(({query}) => { + const searchQueryJSON = SearchUtils.buildSearchQueryJSON(query); + return { + text: searchQueryJSON ? SearchUtils.getSearchHeaderTitle(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query, + singleIcon: Expensicons.History, + query, + keyForList: query, + }; + }); if (recentSearchesData && recentSearchesData.length > 0) { sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); @@ -144,7 +172,7 @@ function SearchRouterList( onSelectRow={onSelectRow} ListItem={SearchRouterItem} containerStyle={[styles.mh100]} - sectionListStyle={[styles.ph5, styles.pb5]} + sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb5]} ref={ref} /> ); diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index 6b32b31aae12..8ec9da4027ed 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -45,8 +45,8 @@ function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus )} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index c5471fa11bce..4567b416c88e 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -71,6 +71,7 @@ function BaseTextInput( suffixContainerStyle = [], suffixStyle = [], contentWidth, + loadingSpinnerStyle, ...inputProps }: BaseTextInputProps, ref: ForwardedRef, @@ -424,7 +425,7 @@ function BaseTextInput( )} {!!inputProps.secureTextEntry && ( diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 32a408e4d348..1dfb494dbe15 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -134,6 +134,9 @@ type CustomBaseTextInputProps = { /** Style for the suffix container */ suffixContainerStyle?: StyleProp; + /** Style for the loading spinner */ + loadingSpinnerStyle?: StyleProp; + /** The width of inner content */ contentWidth?: number; }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 129cc847d2f7..0f97e5ed5e7c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4109,7 +4109,8 @@ const translations = { expenseType: 'Expense type', recentSearches: 'Recent searches', recentChats: 'Recent chats', - searchIn: 'Search in ', + searchIn: 'Search in', + searchPlaceholder: 'Search for something', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 948efcc58482..032263519836 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4162,7 +4162,8 @@ const translations = { expenseType: 'Tipo de gasto', recentSearches: 'Búsquedas recientes', recentChats: 'Chats recientes', - searchIn: 'Buscar en ', + searchIn: 'Buscar en', + searchPlaceholder: 'Busca algo', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', diff --git a/src/styles/index.ts b/src/styles/index.ts index 5e52d6b42b3d..2ad1604b0b39 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3613,10 +3613,10 @@ const styles = (theme: ThemeColors) => lineHeight: 16, }, - searchRouterInputStyle: { + searchRouterTextInputContainerStyle: { borderRadius: variables.componentBorderRadiusSmall, - borderWidth: 2, - borderColor: theme.borderFocus, + borderWidth: 1, + borderBottomWidth: 1, paddingHorizontal: 8, }, From 66fa714921b5608776234a4944cb553d40b8df70 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 1 Oct 2024 14:22:47 +0200 Subject: [PATCH 21/25] fix linter --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 ++ src/components/SelectionList/BaseSelectionList.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index facd60866c69..4c759836e4a3 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -115,6 +115,8 @@ function SearchRouter() { Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, userQuery, false); } }, SEARCH_DEBOUNCE_DELAY), + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps [], ); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 682bc525f6ff..e724aff0f02e 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -633,6 +633,7 @@ function BaseSelectionList( clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, + scrollToIndex, ]); /** Selects row when pressing Enter */ From 11525fa34a45f244c0e9649372056d1a70c4fe6d Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 1 Oct 2024 17:36:00 +0200 Subject: [PATCH 22/25] fix PR comments --- .../Search/SearchRouter/SearchRouterList.tsx | 5 +++-- src/components/SelectionList/BaseListItem.tsx | 11 ----------- .../SelectionList/Search/SearchQueryListItem.tsx | 2 +- src/components/SelectionList/UserListItem.tsx | 3 ++- src/libs/SearchUtils.ts | 5 +++++ src/styles/index.ts | 6 +++--- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 210fe2b6e91c..378b0464cbb1 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -71,7 +71,8 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList } return ( @@ -113,7 +114,7 @@ function SearchRouterList( { text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, singleIcon: Expensicons.MagnifyingGlass, - query: `in:${reportForContextualSearch.reportID}`, + query: SearchUtils.getContextualSuggestionQuery(reportForContextualSearch.reportID), itemStyle: styles.activeComponentBG, keyForList: 'contextualSearch', isContextualSearchItem: true, diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 5f43f8088fc3..31207fdbf1d7 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -4,13 +4,11 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useHover from '@hooks/useHover'; import {useMouseContext} from '@hooks/useMouseContext'; import useSyncFocus from '@hooks/useSyncFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {BaseListItemProps, ListItem} from './types'; @@ -36,7 +34,6 @@ function BaseListItem({ onFocus = () => {}, hoverStyle, onLongPressRow, - hasAnimateInHighlightStyle = false, }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -64,13 +61,6 @@ function BaseListItem({ return rightHandSideComponent; }; - const animatedHighlightStyle = useAnimatedHighlightStyle({ - borderRadius: variables.componentBorderRadius, - shouldHighlight: item?.shouldAnimateInHighlight ?? false, - highlightColor: theme.messageHighlightBG, - backgroundColor: theme.highlightBG, - }); - return ( onDismissError(item)} @@ -109,7 +99,6 @@ function BaseListItem({ onFocus={onFocus} onMouseLeave={handleMouseLeave} tabIndex={item.tabIndex} - wrapperStyle={hasAnimateInHighlightStyle ? [styles.mh5, animatedHighlightStyle] : []} > {typeof children === 'function' ? children(hovered) : children} diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index 8ec9da4027ed..369f527cdeba 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -30,7 +30,7 @@ function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus return ( ({ rightHandSideComponent, onFocus, shouldSyncFocus, + wrapperStyle, pressableStyle, }: UserListItemProps) { const styles = useThemeStyles(); @@ -51,7 +52,7 @@ function UserListItem({ return ( borderRadius: 8, }, - singleIconListItemStyle: { + searchQueryListItemStyle: { alignItems: 'center', flexDirection: 'row', - paddingHorizontal: 16, - paddingVertical: 16, + paddingHorizontal: 12, + paddingVertical: 12, borderRadius: 8, }, From 1b7f6ac743ff6d3976d6127d62debcbbac58e589 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 2 Oct 2024 09:46:40 +0200 Subject: [PATCH 23/25] fix styles --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 ++ src/components/Search/SearchRouter/SearchRouterInput.tsx | 5 +---- src/components/Search/SearchRouter/SearchRouterList.tsx | 4 ++-- src/components/TextInput/BaseTextInput/index.native.tsx | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index bb8fd1fdd3e1..de18bd6de564 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -177,6 +177,8 @@ function SearchRouter() { isFullWidth={isSmallScreenWidth} updateSearch={onSearchChange} routerListRef={listRef} + wrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2, styles.border]} + wrapperFocusedStyle={[styles.borderColorFocus]} /> (false); @@ -62,8 +60,7 @@ function SearchRouterInput({value, setValue, updateSearch, routerListRef, isFull value={value} onChangeText={onChangeText} autoFocus - // containerStyles={[isSmallScreenWidth ? styles.pv3 : styles.pv2, isSmallScreenWidth ? styles.ph5 : styles.ph2]} - loadingSpinnerStyle={styles.mt0} + loadingSpinnerStyle={[styles.mt0, styles.mr2]} role={CONST.ROLE.PRESENTATION} placeholder={translate('search.searchPlaceholder')} autoCapitalize="none" diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 378b0464cbb1..96c11b2fa353 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -133,7 +133,7 @@ function SearchRouterList( }; }); - if (recentSearchesData && recentSearchesData.length > 0) { + if (!currentQuery?.inputQuery && recentSearchesData && recentSearchesData.length > 0) { sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } @@ -173,7 +173,7 @@ function SearchRouterList( onSelectRow={onSelectRow} ListItem={SearchRouterItem} containerStyle={[styles.mh100]} - sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb5]} + sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} ref={ref} /> ); diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index a03e9dbb9aa2..93755cf0ce40 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -66,6 +66,7 @@ function BaseTextInput( prefixContainerStyle = [], prefixStyle = [], contentWidth, + loadingSpinnerStyle, ...props }: BaseTextInputProps, ref: ForwardedRef, @@ -379,7 +380,7 @@ function BaseTextInput( )} {!!inputProps.secureTextEntry && ( From a986ab6c8bec93345e4f62ec6f26233efeedd631 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 2 Oct 2024 09:54:30 +0200 Subject: [PATCH 24/25] fix searchRouterLoading bug --- .../Search/SearchRouter/SearchRouter.tsx | 2 ++ .../Search/SearchRouter/SearchRouterInput.tsx | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index de18bd6de564..76fc42a4f5a9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -35,6 +35,7 @@ function SearchRouter() { const {translate} = useLocalize(); const [betas] = useOnyx(ONYXKEYS.BETAS); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); @@ -179,6 +180,7 @@ function SearchRouter() { routerListRef={listRef} wrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2, styles.border]} wrapperFocusedStyle={[styles.borderColorFocus]} + isSearchingForReports={isSearchingForReports} /> (false); const onChangeText = (text: string) => { From 524eb8b275cacbb14d9aee65cb66911230aac3f6 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 2 Oct 2024 18:19:30 +0200 Subject: [PATCH 25/25] focus list item if it exists --- src/components/Search/SearchRouter/SearchRouter.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 76fc42a4f5a9..b3f147b7ac28 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -50,9 +50,7 @@ function SearchRouter() { return Object.values(recentSearches ?? {}).sort((a, b) => b.timestamp.localeCompare(a.timestamp)); }, [recentSearches]); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: true, - }); + const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { if (!areOptionsInitialized) { return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; @@ -91,6 +89,15 @@ function SearchRouter() { Report.searchInServer(debouncedInputValue.trim()); }, [debouncedInputValue]); + useEffect(() => { + if (!textInputValue && isSearchRouterDisplayed) { + return; + } + listRef.current?.updateAndScrollToFocusedIndex(0); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSearchRouterDisplayed]); + const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => {