diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index e29fe39c0e0a..9a5e13b7b380 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import Onyx from 'react-native-onyx';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import type {Parameters} from 'storybook/internal/types';
-import {MoneyRequestReportContextProvider} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
+import {SearchContextProvider} from '@components/Search/SearchContext';
import ComposeProviders from '@src/components/ComposeProviders';
import HTMLEngineProvider from '@src/components/HTMLEngineProvider';
import {LocaleContextProvider} from '@src/components/LocaleContextProvider';
@@ -23,16 +23,7 @@ Onyx.init({
const decorators = [
(Story: React.ElementType) => (
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 4355ab52da62..54badf640693 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -111,10 +111,10 @@ import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import MoneyReportHeaderStatusBarSkeleton from './MoneyReportHeaderStatusBarSkeleton';
import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
-import {useMoneyRequestReportContext} from './MoneyRequestReportView/MoneyRequestReportContext';
import type {PopoverMenuItem} from './PopoverMenu';
import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
+import {useSearchContext} from './Search/SearchContext';
import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';
import Text from './Text';
@@ -264,7 +264,7 @@ function MoneyReportHeader({
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
- const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
+ const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
const {
options: selectedTransactionsOptions,
@@ -881,8 +881,8 @@ function MoneyReportHeader({
if (!transactionThreadReportID) {
return;
}
- setSelectedTransactionsID([]);
- // We don't need to run the effect on change of setSelectedTransactionsID since it can cause the infinite loop.
+ clearSelectedTransactions(true);
+ // We don't need to run the effect on change of clearSelectedTransactions since it can cause the infinite loop.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionThreadReportID]);
@@ -909,7 +909,7 @@ function MoneyReportHeader({
{
- setSelectedTransactionsID([]);
+ clearSelectedTransactions(true);
turnOffMobileSelectionMode();
}}
/>
@@ -982,7 +982,7 @@ function MoneyReportHeader({
null}
options={selectedTransactionsOptions}
- customText={translate('workspace.common.selected', {count: selectedTransactionsID.length})}
+ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
isSplitButton={false}
shouldAlwaysShowDropdownMenu
/>
@@ -1004,7 +1004,7 @@ function MoneyReportHeader({
null}
options={selectedTransactionsOptions}
- customText={translate('workspace.common.selected', {count: selectedTransactionsID.length})}
+ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
isSplitButton={false}
shouldAlwaysShowDropdownMenu
wrapperStyle={styles.w100}
@@ -1100,11 +1100,11 @@ function MoneyReportHeader({
shouldEnableNewFocusManagement
/>
null}
options={selectedTransactionsOptions}
- customText={translate('workspace.common.selected', {count: selectedTransactionsID.length})}
+ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
isSplitButton={false}
shouldAlwaysShowDropdownMenu
wrapperStyle={[styles.w100, styles.ph5]}
@@ -545,39 +545,39 @@ function MoneyRequestReportActionsList({
0 && selectedTransactionsID.length !== transactions.length}
+ isChecked={selectedTransactionIDs.length === transactions.length}
+ isIndeterminate={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length !== transactions.length}
onPress={() => {
- if (selectedTransactionsID.length !== 0) {
- setSelectedTransactionsID([]);
+ if (selectedTransactionIDs.length !== 0) {
+ clearSelectedTransactions(true);
} else {
- setSelectedTransactionsID(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
+ setSelectedTransactions(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
}
}}
/>
{
- if (selectedTransactionsID.length === transactions.length) {
- setSelectedTransactionsID([]);
+ if (selectedTransactionIDs.length === transactions.length) {
+ clearSelectedTransactions(true);
} else {
- setSelectedTransactionsID(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
+ setSelectedTransactions(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
}
}}
accessibilityLabel={translate('workspace.people.selectAll')}
role="button"
- accessibilityState={{checked: selectedTransactionsID.length === transactions.length}}
+ accessibilityState={{checked: selectedTransactionIDs.length === transactions.length}}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{translate('workspace.people.selectAll')}
void;
- toggleTransaction: (transactionID: string) => void;
- removeTransaction: (transactionID?: string) => void;
- isTransactionSelected: (transactionID: string) => boolean;
-};
-
-const defaultMoneyRequestReportContext = {
- selectedTransactionsID: [],
- setSelectedTransactionsID: () => {},
- toggleTransaction: () => {},
- removeTransaction: () => {},
- isTransactionSelected: () => false,
-};
-
-const Context = React.createContext(defaultMoneyRequestReportContext);
-
-// TODO merge it with SearchContext in follow-up - https://github.com/Expensify/App/issues/59431
-function MoneyRequestReportContextProvider({children}: ChildrenProps) {
- const [selectedTransactions, setSelectedTransactions] = useState([]);
-
- const setSelectedTransactionsID = useCallback((transactionsID: string[]) => {
- setSelectedTransactions(transactionsID);
- }, []);
-
- const toggleTransaction = useCallback((transactionID: string) => {
- setSelectedTransactions((prev) => {
- if (prev.includes(transactionID)) {
- return prev.filter((t) => t !== transactionID);
- }
- return [...prev, transactionID];
- });
- }, []);
-
- const removeTransaction = useCallback((transactionID?: string) => {
- setSelectedTransactions((prev) => {
- return prev.filter((t) => t !== transactionID);
- });
- }, []);
-
- const isTransactionSelected = useCallback((transactionID: string) => selectedTransactions.includes(transactionID), [selectedTransactions]);
-
- const context = useMemo(
- () => ({
- selectedTransactionsID: selectedTransactions,
- setSelectedTransactionsID,
- toggleTransaction,
- isTransactionSelected,
- removeTransaction,
- }),
- [isTransactionSelected, removeTransaction, selectedTransactions, setSelectedTransactionsID, toggleTransaction],
- );
-
- return {children};
-}
-
-function useMoneyRequestReportContext() {
- const context = useContext(Context);
-
- return context;
-}
-
-MoneyRequestReportContextProvider.displayName = 'MoneyRequestReportContextProvider';
-
-export {MoneyRequestReportContextProvider, useMoneyRequestReportContext};
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 5634a845716d..7b338fb1d6ca 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -10,6 +10,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import {useSearchContext} from '@components/Search/SearchContext';
import type {SortOrder} from '@components/Search/types';
import Text from '@components/Text';
import TransactionItemRow from '@components/TransactionItemRow';
@@ -40,7 +41,6 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
-import {useMoneyRequestReportContext} from './MoneyRequestReportContext';
import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader';
import SearchMoneyRequestReportEmptyState from './SearchMoneyRequestReportEmptyState';
import {setActiveTransactionThreadIDs} from './TransactionThreadReportIDRepository';
@@ -137,18 +137,36 @@ function MoneyRequestReportTransactionList({
const {bind} = useHover();
const {isMouseDownOnInput, setMouseUp} = useMouseContext();
- const {selectedTransactionsID, setSelectedTransactionsID, toggleTransaction, isTransactionSelected} = useMoneyRequestReportContext();
+ const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext();
const {selectionMode} = useMobileSelectionMode();
+ const toggleTransaction = useCallback(
+ (transactionID: string) => {
+ let newSelectedTransactionIDs = selectedTransactionIDs;
+ if (selectedTransactionIDs.includes(transactionID)) {
+ newSelectedTransactionIDs = selectedTransactionIDs.filter((t) => t !== transactionID);
+ } else {
+ newSelectedTransactionIDs = [...selectedTransactionIDs, transactionID];
+ }
+ setSelectedTransactions(newSelectedTransactionIDs);
+ },
+ [setSelectedTransactions, selectedTransactionIDs],
+ );
+
+ const isTransactionSelected = useCallback((transactionID: string) => selectedTransactionIDs.includes(transactionID), [selectedTransactionIDs]);
+
useFocusEffect(
useCallback(() => {
return () => {
if (navigationRef?.getRootState()?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
return;
}
- setSelectedTransactionsID([]);
+ clearSelectedTransactions(true);
};
- }, [setSelectedTransactionsID]),
+ // We don't need to run the effect on change of clearSelectedTransactions on every focus.
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []),
);
const handleMouseLeave = (e: React.MouseEvent) => {
@@ -216,15 +234,15 @@ function MoneyRequestReportTransactionList({
{
- if (selectedTransactionsID.length !== 0) {
- setSelectedTransactionsID([]);
+ if (selectedTransactionIDs.length !== 0) {
+ clearSelectedTransactions(true);
} else {
- setSelectedTransactionsID(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
+ setSelectedTransactions(transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID));
}
}}
accessibilityLabel={CONST.ROLE.CHECKBOX}
- isIndeterminate={selectedTransactionsID.length > 0 && selectedTransactionsID.length !== transactions.length}
- isChecked={selectedTransactionsID.length === transactions.length}
+ isIndeterminate={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length !== transactions.length}
+ isChecked={selectedTransactionIDs.length === transactions.length}
/>
{isMediumScreenWidth && {translate('workspace.people.selectAll')}}
diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
index e4e5aadf0a40..e2474a68a911 100644
--- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
+++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
@@ -16,12 +16,13 @@ import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {calculateAmount} from '@libs/IOUUtils';
import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getCleanedTagName} from '@libs/PolicyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
-import {getOriginalMessage, getReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import type {TransactionDetails} from '@libs/ReportUtils';
import {canEditMoneyRequest, getTransactionDetails, getWorkspaceIcon, isIOUReport, isPolicyExpenseChat, isReportApproved, isSettled} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
@@ -66,7 +67,7 @@ function TransactionPreviewContent({
const ownerAccountID = iouReport?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isReportAPolicyExpenseChat = isPolicyExpenseChat(chatReport);
const {amount: requestAmount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails;
- const reportActions = useMemo(() => (iouReport ? (getReportActions(iouReport) ?? {}) : {}), [iouReport]);
+ const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReport?.reportID)}`, {canBeMissing: true});
const transactionPreviewCommonArguments = useMemo(
() => ({
diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx
index 8a9b83bd215d..234aca31ffad 100644
--- a/src/components/Search/SearchContext.tsx
+++ b/src/components/Search/SearchContext.tsx
@@ -1,81 +1,42 @@
import React, {useCallback, useContext, useMemo, useState} from 'react';
-import type {ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types';
import {isMoneyRequestReport} from '@libs/ReportUtils';
import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
-import type {SearchContext, SelectedTransactions} from './types';
+import type {SearchContext, SearchContextData} from './types';
-const defaultSearchContext: SearchContext = {
+const defaultSearchContextData: SearchContextData = {
currentSearchHash: -1,
- shouldTurnOffSelectionMode: false,
selectedTransactions: {},
+ selectedTransactionIDs: [],
selectedReports: [],
+ isOnSearch: false,
+ shouldTurnOffSelectionMode: false,
+};
+
+const defaultSearchContext: SearchContext = {
+ ...defaultSearchContextData,
+ lastSearchType: undefined,
+ isExportMode: false,
+ shouldShowExportModeOption: false,
+ shouldShowFiltersBarLoading: false,
+ setLastSearchType: () => {},
setCurrentSearchHash: () => {},
setSelectedTransactions: () => {},
clearSelectedTransactions: () => {},
- shouldShowFiltersBarLoading: false,
setShouldShowFiltersBarLoading: () => {},
- lastSearchType: undefined,
- setLastSearchType: () => {},
- shouldShowExportModeOption: false,
setShouldShowExportModeOption: () => {},
- isExportMode: false,
setExportMode: () => {},
- isOnSearch: false,
};
const Context = React.createContext(defaultSearchContext);
-function getReportsFromSelectedTransactions(
- data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[],
- selectedTransactions: SelectedTransactions,
-) {
- if (data.length === 0) {
- return [];
- }
-
- if (isReportListItemType(data[0]) || isMoneyRequestReport(data[0])) {
- return data
- .filter(
- (item): item is ReportListItemType =>
- isReportListItemType(item) && isMoneyRequestReport(item) && item.transactions?.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected),
- )
- .map((item) => ({
- reportID: item.reportID,
- action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW,
- total: item.total ?? CONST.DEFAULT_NUMBER_ID,
- policyID: item.policyID,
- }));
- }
-
- if (isTransactionListItemType(data[0])) {
- return data
- .filter((transaction) => transaction.keyForList != null && selectedTransactions[transaction.keyForList]?.isSelected)
- .map((transaction) => ({
- reportID: transaction.reportID,
- action: 'action' in transaction ? (transaction.action ?? CONST.SEARCH.ACTION_TYPES.VIEW) : CONST.SEARCH.ACTION_TYPES.VIEW,
- total: 'amount' in transaction ? (transaction.amount ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID,
- policyID: transaction.policyID,
- }));
- }
-
- return [];
-}
-
function SearchContextProvider({children}: ChildrenProps) {
const [shouldShowExportModeOption, setShouldShowExportModeOption] = useState(false);
const [isExportMode, setExportMode] = useState(false);
-
- const [searchContextData, setSearchContextData] = useState<
- Pick
- >({
- currentSearchHash: defaultSearchContext.currentSearchHash,
- selectedTransactions: defaultSearchContext.selectedTransactions,
- shouldTurnOffSelectionMode: false,
- selectedReports: defaultSearchContext.selectedReports,
- isOnSearch: false,
- });
+ const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false);
+ const [lastSearchType, setLastSearchType] = useState(undefined);
+ const [searchContextData, setSearchContextData] = useState(defaultSearchContextData);
const setCurrentSearchHash = useCallback((searchHash: number) => {
setSearchContextData((prevState) => ({
@@ -84,10 +45,32 @@ function SearchContextProvider({children}: ChildrenProps) {
}));
}, []);
- const setSelectedTransactions = useCallback(
- (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]) => {
+ const setSelectedTransactions: SearchContext['setSelectedTransactions'] = useCallback(
+ (selectedTransactions, data = []) => {
+ if (selectedTransactions instanceof Array) {
+ if (!selectedTransactions.length && !searchContextData.selectedTransactionIDs.length) {
+ return;
+ }
+ return setSearchContextData((prevState) => ({
+ ...prevState,
+ selectedTransactionIDs: selectedTransactions,
+ }));
+ }
+
// When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV.
- const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions);
+ let selectedReports: SearchContext['selectedReports'] = [];
+
+ if (data.length && data.every(isReportListItemType)) {
+ selectedReports = data
+ .filter((item) => isMoneyRequestReport(item) && item.transactions.every(({keyForList}) => selectedTransactions[keyForList]?.isSelected))
+ .map(({reportID, action = CONST.SEARCH.ACTION_TYPES.VIEW, total = CONST.DEFAULT_NUMBER_ID, policyID}) => ({reportID, action, total, policyID}));
+ }
+
+ if (data.length && data.every(isTransactionListItemType)) {
+ selectedReports = data
+ .filter(({keyForList}) => !!keyForList && selectedTransactions[keyForList]?.isSelected)
+ .map(({reportID, action = CONST.SEARCH.ACTION_TYPES.VIEW, amount: total = CONST.DEFAULT_NUMBER_ID, policyID}) => ({reportID, action, total, policyID}));
+ }
setSearchContextData((prevState) => ({
...prevState,
@@ -96,12 +79,17 @@ function SearchContextProvider({children}: ChildrenProps) {
selectedReports,
}));
},
- [],
+ [searchContextData.selectedTransactionIDs.length],
);
- const clearSelectedTransactions = useCallback(
- (searchHash?: number, shouldTurnOffSelectionMode = false) => {
- if (searchHash === searchContextData.currentSearchHash) {
+ const clearSelectedTransactions: SearchContext['clearSelectedTransactions'] = useCallback(
+ (searchHashOrClearIDsFlag, shouldTurnOffSelectionMode = false) => {
+ if (typeof searchHashOrClearIDsFlag === 'boolean') {
+ setSelectedTransactions([]);
+ return;
+ }
+
+ if (searchHashOrClearIDsFlag === searchContextData.currentSearchHash) {
return;
}
setSearchContextData((prevState) => ({
@@ -113,12 +101,9 @@ function SearchContextProvider({children}: ChildrenProps) {
setShouldShowExportModeOption(false);
setExportMode(false);
},
- [searchContextData.currentSearchHash],
+ [searchContextData.currentSearchHash, setSelectedTransactions],
);
- const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false);
- const [lastSearchType, setLastSearchType] = useState(undefined);
-
const searchContext = useMemo(
() => ({
...searchContextData,
@@ -151,6 +136,11 @@ function SearchContextProvider({children}: ChildrenProps) {
return {children};
}
+/**
+ * Note: `selectedTransactionIDs` and `selectedTransactions` are two separate properties.
+ * Setting or clearing one of them does not influence the other.
+ * IDs should be used if transaction details are not required.
+ */
function useSearchContext() {
return useContext(Context);
}
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 28f6f7639b6b..ab24194a715e 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -66,14 +66,27 @@ type SearchStatus = SingularSearchStatus | SingularSearchStatus[];
type SearchGroupBy = ValueOf;
type TableColumnSize = ValueOf;
-type SearchContext = {
+type SearchContextData = {
currentSearchHash: number;
selectedTransactions: SelectedTransactions;
+ selectedTransactionIDs: string[];
selectedReports: SelectedReports[];
- setCurrentSearchHash: (hash: number) => void;
- setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]) => void;
- clearSelectedTransactions: (hash?: number, shouldTurnOffSelectionMode?: boolean) => void;
+ isOnSearch: boolean;
shouldTurnOffSelectionMode: boolean;
+};
+
+type SearchContext = SearchContextData & {
+ setCurrentSearchHash: (hash: number) => void;
+ /** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */
+ setSelectedTransactions: {
+ (selectedTransactionIDs: string[], unused?: undefined): void;
+ (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]): void;
+ };
+ /** If you want to clear `selectedTransactionIDs`, pass `true` as the first argument */
+ clearSelectedTransactions: {
+ (hash?: number, shouldTurnOffSelectionMode?: boolean): void;
+ (clearIDs: true, unused?: undefined): void;
+ };
shouldShowFiltersBarLoading: boolean;
setShouldShowFiltersBarLoading: (shouldShow: boolean) => void;
setLastSearchType: (type: string | undefined) => void;
@@ -82,7 +95,6 @@ type SearchContext = {
setShouldShowExportModeOption: (shouldShow: boolean) => void;
isExportMode: boolean;
setExportMode: (on: boolean) => void;
- isOnSearch: boolean;
};
type ASTNode = {
@@ -168,6 +180,7 @@ export type {
SearchQueryString,
SortOrder,
SearchContext,
+ SearchContextData,
ASTNode,
QueryFilter,
QueryFilters,
diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts
index f7315dcd553e..f73dcb4a97d0 100644
--- a/src/hooks/useSelectedTransactionsActions.ts
+++ b/src/hooks/useSelectedTransactionsActions.ts
@@ -1,7 +1,7 @@
import {useCallback, useMemo, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import * as Expensicons from '@components/Icon/Expensicons';
-import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
+import {useSearchContext} from '@components/Search/SearchContext';
import {deleteMoneyRequest, unholdRequest} from '@libs/actions/IOU';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import {exportReportToCSV} from '@libs/actions/Report';
@@ -43,19 +43,19 @@ function useSelectedTransactionsActions({
session?: Session;
onExportFailed?: () => void;
}) {
- const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
+ const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
const isReportArchived = useReportIsArchived(report?.reportID);
const selectedTransactions = useMemo(
() =>
- selectedTransactionsID.reduce((acc, transactionID) => {
+ selectedTransactionIDs.reduce((acc, transactionID) => {
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
if (transaction) {
acc.push(transaction);
}
return acc;
}, [] as Transaction[]),
- [allTransactions, selectedTransactionsID],
+ [allTransactions, selectedTransactionIDs],
);
const {translate} = useLocalize();
@@ -74,7 +74,7 @@ function useSelectedTransactionsActions({
const handleDeleteTransactions = useCallback(() => {
const iouActions = reportActions.filter((action) => isMoneyRequestAction(action));
- const transactionsWithActions = selectedTransactionsID.map((transactionID) => ({
+ const transactionsWithActions = selectedTransactionIDs.map((transactionID) => ({
transactionID,
action: iouActions.find((action) => {
const IOUTransactionID = (getOriginalMessage(action) as OriginalMessageIOU)?.IOUTransactionID;
@@ -83,12 +83,12 @@ function useSelectedTransactionsActions({
}));
transactionsWithActions.forEach(({transactionID, action}) => action && deleteMoneyRequest(transactionID, action));
- setSelectedTransactionsID([]);
+ clearSelectedTransactions(true);
if (allTransactionsLength - transactionsWithActions.length <= 1) {
turnOffMobileSelectionMode();
}
setIsDeleteModalVisible(false);
- }, [allTransactionsLength, reportActions, selectedTransactionsID, setSelectedTransactionsID]);
+ }, [allTransactionsLength, reportActions, selectedTransactionIDs, clearSelectedTransactions]);
const showDeleteModal = useCallback(() => {
setIsDeleteModalVisible(true);
@@ -99,7 +99,7 @@ function useSelectedTransactionsActions({
}, []);
const computedOptions = useMemo(() => {
- if (!selectedTransactionsID.length) {
+ if (!selectedTransactionIDs.length) {
return [];
}
const options = [];
@@ -145,14 +145,14 @@ function useSelectedTransactionsActions({
icon: Expensicons.Stopwatch,
value: UNHOLD,
onSelected: () => {
- selectedTransactionsID.forEach((transactionID) => {
+ selectedTransactionIDs.forEach((transactionID) => {
const action = getIOUActionForTransactionID(reportActions, transactionID);
if (!action?.childReportID) {
return;
}
unholdRequest(transactionID, action?.childReportID);
});
- setSelectedTransactionsID([]);
+ clearSelectedTransactions(true);
},
});
}
@@ -165,10 +165,10 @@ function useSelectedTransactionsActions({
if (!report) {
return;
}
- exportReportToCSV({reportID: report.reportID, transactionIDList: selectedTransactionsID}, () => {
+ exportReportToCSV({reportID: report.reportID, transactionIDList: selectedTransactionIDs}, () => {
onExportFailed?.();
});
- setSelectedTransactionsID([]);
+ clearSelectedTransactions(true);
},
});
@@ -185,7 +185,7 @@ function useSelectedTransactionsActions({
const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report);
if (canSelectedExpensesBeMoved && canUserPerformWriteAction) {
options.push({
- text: translate('iou.moveExpenses', {count: selectedTransactionsID.length}),
+ text: translate('iou.moveExpenses', {count: selectedTransactionIDs.length}),
icon: Expensicons.DocumentMerge,
value: MOVE,
onSelected: () => {
@@ -195,7 +195,7 @@ function useSelectedTransactionsActions({
});
}
- const canAllSelectedTransactionsBeRemoved = selectedTransactionsID.every((transactionID) => {
+ const canAllSelectedTransactionsBeRemoved = selectedTransactionIDs.every((transactionID) => {
const canRemoveTransaction = canDeleteCardTransactionByLiabilityType(transactionID);
const action = getIOUActionForTransactionID(reportActions, transactionID);
const isActionDeleted = isDeletedAction(action);
@@ -216,12 +216,12 @@ function useSelectedTransactionsActions({
}
return options;
}, [
- selectedTransactionsID,
+ selectedTransactionIDs,
report,
selectedTransactions,
translate,
reportActions,
- setSelectedTransactionsID,
+ clearSelectedTransactions,
onExportFailed,
iouType,
session?.accountID,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 302b5c57f6a1..3e72462a4bf9 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -5,7 +5,6 @@ import type {OnyxEntry} from 'react-native-onyx';
import Onyx, {useOnyx, withOnyx} from 'react-native-onyx';
import ComposeProviders from '@components/ComposeProviders';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import {MoneyRequestReportContextProvider} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext';
@@ -547,7 +546,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
}
return (
-
+
{/* This has to be the first navigator in auth screens. */}
(
{
[SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default,
- [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchMoneyRequestReportHoldReasonPage').default,
+ [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchHoldReasonPage').default,
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default,
[SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default,
},
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index fb3742af7333..eea1ecd4e01e 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -10820,6 +10820,16 @@ function putOnHold(transactionID: string, comment: string, initialReportID: stri
Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID));
}
+function putTransactionsOnHold(transactionsID: string[], comment: string, reportID: string) {
+ transactionsID.forEach((transactionID) => {
+ const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {};
+ if (!childReportID) {
+ return;
+ }
+ putOnHold(transactionID, comment, childReportID);
+ });
+}
+
/**
* Remove expense from HOLD
*/
@@ -11877,6 +11887,7 @@ export {
payInvoice,
payMoneyRequest,
putOnHold,
+ putTransactionsOnHold,
replaceReceipt,
requestMoney,
resetSplitShares,
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 23c8d384f63a..60596b424c97 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -14,7 +14,6 @@ import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/M
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle';
@@ -24,6 +23,7 @@ import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBa
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
+import {useSearchContext} from '@components/Search/SearchContext';
import TextWithCopy from '@components/TextWithCopy';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -157,6 +157,15 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const {reportActions} = usePaginatedReportActions(report.reportID);
+ const {setSelectedTransactions, selectedTransactionIDs} = useSearchContext();
+
+ const removeTransaction = useCallback(
+ (transactionID?: string) => {
+ setSelectedTransactions(selectedTransactionIDs.filter((t) => t !== transactionID));
+ },
+ [setSelectedTransactions, selectedTransactionIDs],
+ );
+
const transactionThreadReportID = useMemo(() => getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), [report.reportID, reportActions, isOffline]);
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
@@ -165,8 +174,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
- const {removeTransaction} = useMoneyRequestReportContext();
-
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]);
diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx
index 3bcd3a84de91..8182f089e3f9 100644
--- a/src/pages/Search/SearchHoldReasonPage.tsx
+++ b/src/pages/Search/SearchHoldReasonPage.tsx
@@ -5,35 +5,34 @@ import useLocalize from '@hooks/useLocalize';
import {clearErrorFields, clearErrors} from '@libs/actions/FormActions';
import {holdMoneyRequestOnSearch} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
-import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getFieldRequiredErrors} from '@libs/ValidationUtils';
+import type {SearchReportParamList} from '@navigation/types';
import HoldReasonFormView from '@pages/iou/HoldReasonFormView';
+import {putTransactionsOnHold} from '@userActions/IOU';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Route} from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
-type SearchHoldReasonPageRouteParams = {
- /** Link to previous page */
- backTo: Route;
-};
-
-type SearchHoldReasonPageProps = {
- /** Navigation route context info provided by react navigation */
- route: PlatformStackRouteProp<{params?: SearchHoldReasonPageRouteParams}>;
-};
-
-function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) {
+function SearchHoldReasonPage({route}: PlatformStackScreenProps>) {
const {translate} = useLocalize();
+ const {backTo = '', reportID} = route.params ?? {};
+ const context = useSearchContext();
- const {currentSearchHash, selectedTransactions, clearSelectedTransactions} = useSearchContext();
- const {backTo = ''} = route.params ?? {};
+ const onSubmit = useCallback(
+ ({comment}: FormOnyxValues) => {
+ if (route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS) {
+ putTransactionsOnHold(context.selectedTransactionIDs, comment, reportID);
+ context.clearSelectedTransactions(true);
+ } else {
+ holdMoneyRequestOnSearch(context.currentSearchHash, Object.keys(context.selectedTransactions), comment);
+ context.clearSelectedTransactions();
+ }
- const selectedTransactionIDs = Object.keys(selectedTransactions);
- const onSubmit = (values: FormOnyxValues) => {
- holdMoneyRequestOnSearch(currentSearchHash, selectedTransactionIDs, values.comment);
- clearSelectedTransactions();
- Navigation.goBack();
- };
+ Navigation.goBack();
+ },
+ [route.name, context, reportID],
+ );
const validate = useCallback(
(values: FormOnyxValues) => {
diff --git a/src/pages/Search/SearchMoneyRequestReportHoldReasonPage.tsx b/src/pages/Search/SearchMoneyRequestReportHoldReasonPage.tsx
deleted file mode 100644
index f315a7f86c32..000000000000
--- a/src/pages/Search/SearchMoneyRequestReportHoldReasonPage.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, {useCallback, useEffect} from 'react';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
-import useLocalize from '@hooks/useLocalize';
-import {putOnHold} from '@libs/actions/IOU';
-import Navigation from '@libs/Navigation/Navigation';
-import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {SearchReportParamList} from '@libs/Navigation/types';
-import {getIOUActionForReportID} from '@libs/ReportActionsUtils';
-import {getFieldRequiredErrors} from '@libs/ValidationUtils';
-import HoldReasonFormView from '@pages/iou/HoldReasonFormView';
-import {clearErrorFields, clearErrors} from '@userActions/FormActions';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type SCREENS from '@src/SCREENS';
-import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
-
-function SearchMoneyRequestReportHoldReasonPage({route}: PlatformStackScreenProps) {
- const {translate} = useLocalize();
-
- const {backTo, reportID} = route.params;
- const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
-
- const onSubmit = (values: FormOnyxValues) => {
- selectedTransactionsID.forEach((transactionID) => {
- const iouAction = getIOUActionForReportID(reportID, transactionID);
- const transactionThreadReportID = iouAction?.childReportID;
- if (!transactionThreadReportID) {
- return;
- }
-
- putOnHold(transactionID, values.comment, transactionThreadReportID);
- });
- setSelectedTransactionsID([]);
- Navigation.goBack();
- };
-
- const validate = useCallback(
- (values: FormOnyxValues) => {
- const errors: FormInputErrors = getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]);
-
- if (!values.comment) {
- errors.comment = translate('common.error.fieldRequired');
- }
-
- return errors;
- },
- [translate],
- );
-
- useEffect(() => {
- clearErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM);
- clearErrorFields(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM);
- }, []);
-
- return (
-
- );
-}
-
-SearchMoneyRequestReportHoldReasonPage.displayName = 'SearchMoneyRequestReportHoldReasonPage';
-
-export default SearchMoneyRequestReportHoldReasonPage;
diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx
index 43a796ab1d8c..2d4b0c240212 100644
--- a/src/pages/iou/request/step/IOURequestEditReport.tsx
+++ b/src/pages/iou/request/step/IOURequestEditReport.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {useOnyx} from 'react-native-onyx';
-import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
+import {useSearchContext} from '@components/Search/SearchContext';
import type {ListItem} from '@components/SelectionList/types';
import {changeTransactionsReport} from '@libs/actions/Transaction';
import Navigation from '@libs/Navigation/Navigation';
@@ -20,17 +20,17 @@ type IOURequestEditReportProps = WithWritableReportOrNotFoundProps {
- if (selectedTransactionsID.length === 0) {
+ if (selectedTransactionIDs.length === 0) {
return;
}
if (item.value !== transactionReport?.reportID) {
- changeTransactionsReport(selectedTransactionsID, item.value);
- setSelectedTransactionsID([]);
+ changeTransactionsReport(selectedTransactionIDs, item.value);
+ clearSelectedTransactions(true);
}
Navigation.dismissModalWithReport({reportID: item.value});
};
diff --git a/tests/ui/ReportListItemHeaderTest.tsx b/tests/ui/ReportListItemHeaderTest.tsx
index a2998d60d6ff..a90350b8793e 100644
--- a/tests/ui/ReportListItemHeaderTest.tsx
+++ b/tests/ui/ReportListItemHeaderTest.tsx
@@ -23,10 +23,18 @@ jest.mock('@components/AvatarWithDisplayName.tsx');
const mockSearchContext = {
currentSearchHash: 12345,
selectedReports: {},
- setSelectedReports: jest.fn(),
selectedTransactionIDs: [],
- setSelectedTransactionIDs: jest.fn(),
- clearSelectedItems: jest.fn(),
+ selectedTransactions: {},
+ isOnSearch: false,
+ shouldTurnOffSelectionMode: false,
+ setSelectedReports: jest.fn(),
+ clearSelectedTransactions: jest.fn(),
+ setLastSearchType: jest.fn(),
+ setCurrentSearchHash: jest.fn(),
+ setSelectedTransactions: jest.fn(),
+ setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowExportModeOption: jest.fn(),
+ setExportMode: jest.fn(),
};
const mockPersonalDetails: Record = {