diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 7be80589ac50..73091abf045b 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,13 +1,14 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import {isMoneyRequestReport} from '@libs/ReportUtils'; -import * as SearchUIUtils from '@libs/SearchUIUtils'; +import {isReportListItemType} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {SearchContext, SelectedTransactions} from './types'; const defaultSearchContext = { currentSearchHash: -1, + shouldTurnOffSelectionMode: false, selectedTransactions: {}, selectedReports: [], setCurrentSearchHash: () => {}, @@ -25,17 +26,18 @@ function getReportsFromSelectedTransactions(data: TransactionListItemType[] | Re return (data ?? []) .filter( (item): item is ReportListItemType => - SearchUIUtils.isReportListItemType(item) && + isReportListItemType(item) && isMoneyRequestReport(item) && item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), ) - .map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? 0, policyID: item.policyID ?? ''})); + .map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? CONST.DEFAULT_NUMBER_ID, policyID: item.policyID})); } function SearchContextProvider({children}: ChildrenProps) { - const [searchContextData, setSearchContextData] = useState>({ + const [searchContextData, setSearchContextData] = useState>({ currentSearchHash: defaultSearchContext.currentSearchHash, selectedTransactions: defaultSearchContext.selectedTransactions, + shouldTurnOffSelectionMode: false, selectedReports: defaultSearchContext.selectedReports, }); @@ -53,17 +55,19 @@ function SearchContextProvider({children}: ChildrenProps) { setSearchContextData((prevState) => ({ ...prevState, selectedTransactions, + shouldTurnOffSelectionMode: false, selectedReports, })); }, []); const clearSelectedTransactions = useCallback( - (searchHash?: number) => { + (searchHash?: number, shouldTurnOffSelectionMode = false) => { if (searchHash === searchContextData.currentSearchHash) { return; } setSearchContextData((prevState) => ({ ...prevState, + shouldTurnOffSelectionMode, selectedTransactions: {}, selectedReports: [], })); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 70398194cc84..d9c2d44adb6b 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -183,7 +183,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const paymentData = ( selectedReports.length - ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[report.policyID]})) + ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[`${report.policyID}`]})) : Object.values(selectedTransactions).map((transaction) => ({ reportID: transaction.reportID, amount: transaction.amount, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 03b6c820da00..1f5443b11a58 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -15,16 +15,25 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as SearchActions from '@libs/actions/Search'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {createTransactionThread, search} from '@libs/actions/Search'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import memoize from '@libs/memoize'; import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as SearchQueryUtils from '@libs/SearchQueryUtils'; -import * as SearchUIUtils from '@libs/SearchUIUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {generateReportID} from '@libs/ReportUtils'; +import {buildSearchQueryString} from '@libs/SearchQueryUtils'; +import { + getListItem, + getSections, + getSortedSections, + isReportActionListItemType, + isReportListItemType, + isSearchResultsEmpty as isSearchResultsEmptyUtil, + isTransactionListItemType, + shouldShowYear as shouldShowYearUtil, +} from '@libs/SearchUIUtils'; +import {isOnHold} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; import EmptySearchView from '@pages/Search/EmptySearchView'; @@ -57,7 +66,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri isSelected: true, canDelete: item.canDelete, canHold: item.canHold, - isHeld: TransactionUtils.isOnHold(item), + isHeld: isOnHold(item), canUnhold: item.canUnhold, action: item.action, reportID: item.reportID, @@ -77,14 +86,14 @@ function mapToItemWithSelectionInfo( canSelectMultiple: boolean, shouldAnimateInHighlight: boolean, ) { - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { return { ...item, shouldAnimateInHighlight, }; } - return SearchUIUtils.isTransactionListItemType(item) + return isTransactionListItemType(item) ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight) : { ...item, @@ -107,7 +116,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact isSelected: true, canDelete: item.canDelete, canHold: item.canHold, - isHeld: TransactionUtils.isOnHold(item), + isHeld: isOnHold(item), canUnhold: item.canUnhold, action: item.action, reportID: item.reportID, @@ -127,8 +136,16 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const navigation = useNavigation>(); const isFocused = useIsFocused(); const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState(undefined); - const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading, lastSearchType, setLastSearchType} = - useSearchContext(); + const { + setCurrentSearchHash, + setSelectedTransactions, + selectedTransactions, + clearSelectedTransactions, + shouldTurnOffSelectionMode, + setShouldShowStatusBarLoading, + lastSearchType, + setLastSearchType, + } = useSearchContext(); const {selectionMode} = useMobileSelectionMode(false); const [offset, setOffset] = useState(0); @@ -158,6 +175,13 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo setCurrentSearchHash(hash); }, [hash, clearSelectedTransactions, setCurrentSearchHash]); + useEffect(() => { + const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); + if (selectedKeys.length === 0 && selectionMode?.isEnabled && shouldTurnOffSelectionMode) { + turnOffMobileSelectionMode(); + } + }, [selectedTransactions, selectionMode?.isEnabled, shouldTurnOffSelectionMode]); + useEffect(() => { const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); if (!isSmallScreenWidth) { @@ -176,12 +200,12 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo return; } - SearchActions.search({queryJSON, offset}); + search({queryJSON, offset}); }, [isOffline, offset, queryJSON]); const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { - if (SearchUIUtils.isTransactionListItemType(item) || SearchUIUtils.isReportActionListItemType(item)) { + if (isTransactionListItemType(item) || isReportActionListItemType(item)) { return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding; } @@ -229,14 +253,14 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const shouldShowLoadingState = !isOffline && !isDataLoaded; const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; - const isSearchResultsEmpty = !searchResults?.data || SearchUIUtils.isSearchResultsEmpty(searchResults); + const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults); const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty); const data = useMemo(() => { if (searchResults === undefined) { return []; } - return SearchUIUtils.getSections(type, status, searchResults.data, searchResults.search); + return getSections(type, status, searchResults.data, searchResults.search); }, [searchResults, status, type]); useEffect(() => { @@ -260,7 +284,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo newTransactionList[transaction.transactionID] = { action: transaction.action, canHold: transaction.canHold, - isHeld: TransactionUtils.isOnHold(transaction), + isHeld: isOnHold(transaction), canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, @@ -281,7 +305,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo newTransactionList[transaction.transactionID] = { action: transaction.action, canHold: transaction.canHold, - isHeld: TransactionUtils.isOnHold(transaction), + isHeld: isOnHold(transaction), canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, @@ -328,8 +352,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo return {null}; } - const ListItem = SearchUIUtils.getListItem(type, status); - const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder); + const ListItem = getListItem(type, status); + const sortedData = getSortedSections(type, status, data, sortBy, sortOrder); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const sortedSelectedData = sortedData.map((item) => { const baseKey = isChat @@ -364,10 +388,10 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo } const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { return; } - if (SearchUIUtils.isTransactionListItemType(item)) { + if (isTransactionListItemType(item)) { if (!item.keyForList) { return; } @@ -398,21 +422,21 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID; - let reportID = SearchUIUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID; + let reportID = isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID; if (!reportID) { return; } // If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user - if (SearchUIUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) { - reportID = ReportUtils.generateReportID(); - SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID); + if (isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) { + reportID = generateReportID(); + createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID); } const backTo = Navigation.getActiveRoute(); - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { const reportActionID = item.reportActionID; Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo})); return; @@ -448,11 +472,11 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo }; const onSortPress = (column: SearchColumnType, order: SortOrder) => { - const newQuery = SearchQueryUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); + const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); navigation.setParams({q: newQuery}); }; - const shouldShowYear = SearchUIUtils.shouldShowYear(searchResults?.data); + const shouldShowYear = shouldShowYearUtil(searchResults?.data); const shouldShowSorting = !Array.isArray(status) && sortableSearchStatuses.includes(status); return ( @@ -477,7 +501,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo ) } isSelected={(item) => - status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUIUtils.isReportListItemType(item) + status !== CONST.SEARCH.STATUS.EXPENSE.ALL && isReportListItemType(item) ? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected) : !!item.isSelected } @@ -501,7 +525,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo onSelectRow={openReport} getItemHeight={getItemHeightMemoized} shouldSingleExecuteRowSelect - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} shouldPreventDefault={false} listHeaderWrapperStyle={[styles.ph8, styles.pt3]} containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0a402358d73e..3dc408cc27c3 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -39,7 +39,7 @@ type SelectedTransactions = Record; /** Model of selected reports */ type SelectedReports = { reportID: string; - policyID: string; + policyID: string | undefined; action: ValueOf; total: number; }; @@ -65,7 +65,8 @@ type SearchContext = { selectedReports: SelectedReports[]; setCurrentSearchHash: (hash: number) => void; setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => void; - clearSelectedTransactions: (hash?: number) => void; + clearSelectedTransactions: (hash?: number, shouldTurnOffSelectionMode?: boolean) => void; + shouldTurnOffSelectionMode: boolean; shouldShowStatusBarLoading: boolean; setShouldShowStatusBarLoading: (shouldShow: boolean) => void; setLastSearchType: (type: string | undefined) => void; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a642440c1500..f2a97d811dfb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1122,7 +1122,7 @@ function getWorkspaceAccountID(policyID?: string) { return policy.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; } -function hasVBBA(policyID: string) { +function hasVBBA(policyID: string | undefined) { const policy = getPolicy(policyID); return !!policy?.achAccount?.bankAccountID; } diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index b1e6ebac8f61..49b59ac59f13 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -5,6 +5,7 @@ import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedVa import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; +import {useSearchContext} from '@components/Search/SearchContext'; import SearchStatusBar from '@components/Search/SearchStatusBar'; import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; import useLocalize from '@hooks/useLocalize'; @@ -22,6 +23,7 @@ import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import SearchSelectionModeHeader from './SearchSelectionModeHeader'; import SearchTypeMenu from './SearchTypeMenu'; +import useHandleBackButton from './useHandleBackButton'; const TOO_CLOSE_TO_TOP_DISTANCE = 10; const TOO_CLOSE_TO_BOTTOM_DISTANCE = 10; @@ -35,6 +37,17 @@ function SearchPageBottomTab() { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + const {clearSelectedTransactions} = useSearchContext(); + + const handleBackButtonPress = useCallback(() => { + if (!selectionMode?.isEnabled) { + return false; + } + clearSelectedTransactions(undefined, true); + return true; + }, [selectionMode, clearSelectedTransactions]); + + useHandleBackButton(handleBackButtonPress); const scrollOffset = useSharedValue(0); const topBarOffset = useSharedValue(StyleUtils.searchHeaderHeight); diff --git a/src/pages/Search/useHandleBackButton/index.android.ts b/src/pages/Search/useHandleBackButton/index.android.ts new file mode 100644 index 000000000000..69c36e8e06d4 --- /dev/null +++ b/src/pages/Search/useHandleBackButton/index.android.ts @@ -0,0 +1,11 @@ +import {useEffect} from 'react'; +import {BackHandler} from 'react-native'; +import type UseHandleBackButtonCallback from './type'; + +export default function useHandleBackButton(callback: UseHandleBackButtonCallback) { + useEffect(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', callback); + + return () => backHandler.remove(); + }, [callback]); +} diff --git a/src/pages/Search/useHandleBackButton/index.ts b/src/pages/Search/useHandleBackButton/index.ts new file mode 100644 index 000000000000..691e2dd0378b --- /dev/null +++ b/src/pages/Search/useHandleBackButton/index.ts @@ -0,0 +1,4 @@ +import type UseHandleBackButtonCallback from './type'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function useHandleBackButton(_callback: UseHandleBackButtonCallback) {} diff --git a/src/pages/Search/useHandleBackButton/type.ts b/src/pages/Search/useHandleBackButton/type.ts new file mode 100644 index 000000000000..e32f48555bde --- /dev/null +++ b/src/pages/Search/useHandleBackButton/type.ts @@ -0,0 +1,3 @@ +type UseHandleBackButtonCallback = () => boolean; + +export default UseHandleBackButtonCallback;