diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4c98fc31c8d2..8df62a88e343 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -307,7 +307,7 @@ function MoneyReportHeader({ const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const {selectedTransactionIDs, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey} = useSearchContext(); + const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey} = useSearchContext(); const beginExportWithTemplate = useCallback( (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => { @@ -1318,9 +1318,10 @@ function MoneyReportHeader({ } // it's deleting transaction but not the report which leads to bug (that is actually also on staging) // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute - InteractionManager.runAfterInteractions(() => - deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, duplicateTransactions, duplicateTransactionViolations), - ); + InteractionManager.runAfterInteractions(() => { + deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, duplicateTransactions, duplicateTransactionViolations); + removeTransaction(transaction.transactionID); + }); goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, requestParentReportAction, false); } diff --git a/src/components/SelectionList/Search/TransactionGroupListItem.tsx b/src/components/SelectionList/Search/TransactionGroupListItem.tsx index 2dc280038a7b..7356457917b7 100644 --- a/src/components/SelectionList/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionList/Search/TransactionGroupListItem.tsx @@ -256,8 +256,13 @@ function TransactionGroupListItem({ useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus); + const pendingAction = + (item.pendingAction ?? (groupItem.transactions.length > 0 && groupItem.transactions.every((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE))) + ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + : undefined; + return ( - + ({ )} {transactions.map((transaction) => ( - onCheckboxPress?.(transaction as unknown as TItem)} - columns={currentColumns} - onButtonPress={() => { - openReportInRHP(transaction); - }} - style={[styles.noBorderRadius, shouldUseNarrowLayout ? [styles.p3, styles.pt2] : [styles.ph3, styles.pv1Half]]} - isReportItemChild - isInSingleTransactionReport={groupItem.transactions.length === 1} - areAllOptionalColumnsHidden={areAllOptionalColumnsHidden} - /> + pendingAction={transaction.pendingAction} + > + onCheckboxPress?.(transaction as unknown as TItem)} + columns={currentColumns} + onButtonPress={() => { + openReportInRHP(transaction); + }} + style={[styles.noBorderRadius, shouldUseNarrowLayout ? [styles.p3, styles.pt2] : [styles.ph3, styles.pv1Half]]} + isReportItemChild + isInSingleTransactionReport={groupItem.transactions.length === 1} + areAllOptionalColumnsHidden={areAllOptionalColumnsHidden} + /> + ))} {shouldDisplayShowMoreButton && !shouldDisplayLoadingIndicator && ( diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 396092efabfb..6e37ab4abe74 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1795,7 +1795,11 @@ function getSortedReportActionData(data: ReportActionListItemType[], localeCompa * Checks if the search results contain any data, useful for determining if the search results are empty. */ function isSearchResultsEmpty(searchResults: SearchResults) { - return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)); + return !Object.keys(searchResults?.data).some( + (key) => + key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION) && + (searchResults?.data[key as keyof typeof searchResults.data] as SearchTransaction)?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); } /** diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index d7c15144d8d7..17eb0ac83f33 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -488,8 +488,31 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { } function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { - const {optimisticData, finallyData} = getOnyxLoadingData(hash); - API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); + const {optimisticData: loadingOptimisticData, finallyData} = getOnyxLoadingData(hash); + const optimisticData: OnyxUpdate[] = [ + ...loadingOptimisticData, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: Object.fromEntries( + transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]), + ) as Partial, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: Object.fromEntries( + transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {pendingAction: null}]), + ) as Partial, + }, + }, + ]; + API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, failureData, finallyData}); } type Params = Record; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 378198ce4629..255a319d75e0 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -536,11 +536,11 @@ function SearchPage({route}: SearchPageProps) { } setIsDeleteExpensesConfirmModalVisible(false); - deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); // Translations copy for delete modal depends on amount of selected items, // We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural InteractionManager.runAfterInteractions(() => { + deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); clearSelectedTransactions(); }); }; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index e0d025f686d5..69eab20718a7 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -2214,6 +2214,64 @@ describe('SearchUIUtils', () => { }); }); + describe('Test isSearchResultsEmpty', () => { + it('should return true when all transactions have delete pending action', () => { + const results: OnyxTypes.SearchResults = { + data: { + personalDetailsList: {}, + // eslint-disable-next-line @typescript-eslint/naming-convention + transactions_1805965960759424086: { + accountID: 2074551, + amount: 0, + canDelete: false, + canHold: true, + canUnhold: false, + category: 'Employee Meals Remote (Fringe Benefit)', + action: 'approve', + allActions: ['approve'], + comment: { + comment: '', + }, + created: '2025-05-26', + currency: 'USD', + hasEReceipt: false, + isFromOneTransactionReport: true, + managerID: adminAccountID, + merchant: '(none)', + modifiedAmount: -1000, + modifiedCreated: '2025-05-22', + modifiedCurrency: 'USD', + modifiedMerchant: 'Costco Wholesale', + parentTransactionID: '', + policyID: '137DA25D273F2423', + receipt: { + source: 'https://www.expensify.com/receipts/fake.jpg', + state: CONST.IOU.RECEIPT_STATE.SCAN_COMPLETE, + }, + reportID: '6523565988285061', + reportType: 'expense', + tag: '', + transactionID: '1805965960759424086', + transactionThreadReportID: '4139222832581831', + transactionType: 'cash', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + convertedAmount: -5000, + convertedCurrency: 'USD', + }, + }, + search: { + type: 'expense', + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + offset: 0, + hasMoreResults: false, + hasResults: true, + isLoading: false, + }, + }; + expect(SearchUIUtils.isSearchResultsEmpty(results)).toBe(true); + }); + }); + test('Should show `View` to overlimit approver', () => { Onyx.merge(ONYXKEYS.SESSION, {accountID: overlimitApproverAccountID}); searchResults.data[`policy_${policyID}`].role = CONST.POLICY.ROLE.USER;