diff --git a/src/App.tsx b/src/App.tsx index 21025d34a661..98b5d4afeb1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; +import {SearchContextProvider} from './components/Search/SearchContext'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; @@ -91,6 +92,7 @@ function App({url}: AppProps) { VolumeContextProvider, VideoPopoverMenuContextProvider, KeyboardProvider, + SearchContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 13d44ee883be..cacba632165e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5062,6 +5062,9 @@ const CONST = { DONE: 'done', PAID: 'paid', VIEW: 'view', + REVIEW: 'review', + HOLD: 'hold', + UNHOLD: 'unhold', }, TRANSACTION_TYPE: { CASH: 'cash', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d548297cb854..fea0381a03b9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -53,6 +53,11 @@ const ROUTES = { getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, }, + TRANSACTION_HOLD_REASON_RHP: { + route: '/search/:query/hold/:transactionID', + getRoute: (query: string, transactionID: string) => `search/${query}/hold/${transactionID}` as const, + }, + // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', FLAG_COMMENT: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2e3f7acc5b0..d19a44cc74aa 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -30,6 +30,7 @@ const SCREENS = { SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', REPORT_RHP: 'Search_Report_RHP', + TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index a520693cff57..8c980838b841 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -74,8 +74,11 @@ type ReceiptImageProps = ( /** The size of the fallback icon */ fallbackIconSize?: number; - /** The colod of the fallback icon */ + /** The color of the fallback icon */ fallbackIconColor?: string; + + /** The background color of fallback icon */ + fallbackIconBackground?: string; }; function ReceiptImage({ @@ -93,6 +96,7 @@ function ReceiptImage({ fallbackIconSize, shouldUseInitialObjectPosition = false, fallbackIconColor, + fallbackIconBackground, }: ReceiptImageProps) { const styles = useThemeStyles(); @@ -129,6 +133,7 @@ function ReceiptImage({ fallbackIcon={fallbackIcon} fallbackIconSize={fallbackIconSize} fallbackIconColor={fallbackIconColor} + fallbackIconBackground={fallbackIconBackground} objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP} /> ); diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx new file mode 100644 index 000000000000..3911780d3965 --- /dev/null +++ b/src/components/Search/SearchContext.tsx @@ -0,0 +1,58 @@ +import React, {useCallback, useContext, useMemo, useState} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {SearchContext} from './types'; + +const defaultSearchContext = { + currentSearchHash: -1, + selectedTransactionIDs: [], + setCurrentSearchHash: () => {}, + setSelectedTransactionIds: () => {}, +}; + +const Context = React.createContext(defaultSearchContext); + +function SearchContextProvider({children}: ChildrenProps) { + const [searchContextData, setSearchContextData] = useState>({ + currentSearchHash: defaultSearchContext.currentSearchHash, + selectedTransactionIDs: defaultSearchContext.selectedTransactionIDs, + }); + + const setCurrentSearchHash = useCallback( + (searchHash: number) => { + setSearchContextData({ + ...searchContextData, + currentSearchHash: searchHash, + }); + }, + [searchContextData], + ); + + const setSelectedTransactionIds = useCallback( + (selectedTransactionIDs: string[]) => { + setSearchContextData({ + ...searchContextData, + selectedTransactionIDs, + }); + }, + [searchContextData], + ); + + const searchContext = useMemo( + () => ({ + ...searchContextData, + setCurrentSearchHash, + setSelectedTransactionIds, + }), + [searchContextData, setCurrentSearchHash, setSelectedTransactionIds], + ); + + return {children}; +} + +function useSearchContext() { + return useContext(Context); +} + +SearchContextProvider.displayName = 'SearchContextProvider'; + +export {SearchContextProvider, useSearchContext}; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 8445cb3bc72e..76e0ca3563ee 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -13,7 +13,6 @@ import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import * as ReportUtils from '@libs/ReportUtils'; -import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; @@ -25,8 +24,10 @@ import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import {useSearchContext} from './SearchContext'; import SearchListWithHeader from './SearchListWithHeader'; import SearchPageHeader from './SearchPageHeader'; +import type {SearchColumnType, SortOrder} from './types'; type SearchProps = { query: SearchQuery; @@ -47,6 +48,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isLargeScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); + const {setCurrentSearchHash} = useSearchContext(); const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType) => { @@ -83,6 +85,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { return; } + setCurrentSearchHash(hash); SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [hash, isOffline]); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 3ebc2797947a..cff74fe08a0a 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; +import type CONST from '@src/CONST'; + /** Model of the selected transaction */ type SelectedTransactionInfo = { /** Whether the transaction is selected */ @@ -13,5 +16,14 @@ type SelectedTransactionInfo = { /** Model of selected results */ type SelectedTransactions = Record; -// eslint-disable-next-line import/prefer-default-export -export type {SelectedTransactionInfo, SelectedTransactions}; +type SortOrder = ValueOf; +type SearchColumnType = ValueOf; + +type SearchContext = { + currentSearchHash: number; + selectedTransactionIDs: string[]; + setCurrentSearchHash: (hash: number) => void; + setSelectedTransactionIds: (selectedTransactionIds: string[]) => void; +}; + +export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext}; diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 5af3d84bf32f..ad77070c1b99 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -1,46 +1,78 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import Badge from '@components/Badge'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; +import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; +import * as SearchActions from '@userActions/Search'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; +import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; + +const actionTranslationsMap: Record = { + view: 'common.view', + review: 'common.review', + done: 'common.done', + paid: 'iou.settledExpensify', + hold: 'iou.hold', + unhold: 'iou.unhold', +}; type ActionCellProps = { - onButtonPress: () => void; - action?: string; + action?: SearchTransactionAction; + transactionID?: string; isLargeScreenWidth?: boolean; isSelected?: boolean; + goToItem: () => void; }; -function ActionCell({onButtonPress, action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false}: ActionCellProps) { +function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const theme = useTheme(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + + const {currentSearchHash} = useSearchContext(); + + const onButtonPress = useCallback(() => { + if (!transactionID) { + return; + } + + if (action === CONST.SEARCH.ACTION_TYPES.HOLD) { + Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(CONST.SEARCH.TAB.ALL, transactionID)); + } else if (action === CONST.SEARCH.ACTION_TYPES.UNHOLD) { + SearchActions.unholdMoneyRequestOnSearch(currentSearchHash, [transactionID]); + } + }, [action, currentSearchHash, transactionID]); + if (!isLargeScreenWidth) { return null; } + const text = translate(actionTranslationsMap[action]); + if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) { - const buttonTextKey = action === CONST.SEARCH.ACTION_TYPES.PAID ? 'iou.settledExpensify' : 'common.done'; return ( + ); + } + return (