From 3a0033a41313ceb60fbc69946e414bcca42b9f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 13:44:20 +0100 Subject: [PATCH 01/30] Reapply Allow rejecting expenses in bulk in NewDot --- src/CONST/index.ts | 1 + src/ROUTES.ts | 1 + src/SCREENS.ts | 2 + src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../ModalStackNavigators/index.tsx | 8 +++ .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 5 ++ src/libs/actions/IOU.ts | 56 +++++++++++++++++-- src/libs/actions/Search.ts | 26 +++++++++ src/pages/Search/SearchHoldReasonPage.tsx | 5 -- src/pages/Search/SearchPage.tsx | 2 + src/pages/Search/SearchRejectReasonPage.tsx | 49 ++++++++++++++++ src/pages/iou/RejectReasonFormView.tsx | 2 +- 23 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 src/pages/Search/SearchRejectReasonPage.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4840da1ed82fd..152bf7585f703 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6552,6 +6552,7 @@ const CONST = { HOLD: 'hold', UNHOLD: 'unhold', DELETE: 'delete', + REJECT: 'reject', CHANGE_REPORT: 'changeReport', }, TRANSACTION_TYPE: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1f9dd1a5849f4..41f04d10cfcdc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -111,6 +111,7 @@ const ROUTES = { }, }, TRANSACTION_HOLD_REASON_RHP: 'search/hold', + SEARCH_REJECT_REASON_RHP: 'search/reject', MOVE_TRANSACTIONS_SEARCH_RHP: 'search/move-transactions', // 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 diff --git a/src/SCREENS.ts b/src/SCREENS.ts index d1b2ac8e786c3..74db7555362b8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -46,6 +46,7 @@ const SCREENS = { MONEY_REQUEST_REPORT: 'Search_Money_Request_Report', MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: 'Search_Money_Request_Report_Verify_Account', MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: 'Search_Money_Request_Report_Hold_Transactions', + MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS: 'Search_Money_Request_Report_Reject_Transactions', REPORT_RHP: 'Search_Report_RHP', REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account', ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', @@ -91,6 +92,7 @@ const SCREENS = { SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP', ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', + SEARCH_REJECT_REASON_RHP: 'Search_Reject_Reason_RHP', TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP: 'Search_Transactions_Change_Report_RHP', }, SETTINGS: { diff --git a/src/languages/de.ts b/src/languages/de.ts index 46435bec2257d..ce15a67b0f492 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6282,6 +6282,7 @@ ${amount} für ${merchant} - ${date}`, delete: 'Löschen', hold: 'Halten', unhold: 'Halten entfernen', + reject: 'Ablehnen', noOptionsAvailable: 'Keine Optionen verfügbar für die ausgewählte Gruppe von Ausgaben.', }, filtersHeader: 'Filter', diff --git a/src/languages/en.ts b/src/languages/en.ts index 97575c064d1b6..0eb8f305af08c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6251,6 +6251,7 @@ const translations = { delete: 'Delete', hold: 'Hold', unhold: 'Remove hold', + reject: 'Reject', noOptionsAvailable: 'No options available for the selected group of expenses.', }, filtersHeader: 'Filters', diff --git a/src/languages/es.ts b/src/languages/es.ts index ae67b62a9c61c..0a2c99c53e98c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5919,6 +5919,7 @@ ${amount} para ${merchant} - ${date}`, delete: 'Eliminar', hold: 'Retener', unhold: 'Desbloquear', + reject: 'Rechazar', noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.', }, filtersHeader: 'Filtros', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 24abfc8b1365e..242c484a163f9 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6289,6 +6289,7 @@ ${amount} pour ${merchant} - ${date}`, delete: 'Supprimer', hold: 'Attente', unhold: 'Supprimer la suspension', + reject: 'Refuser', noOptionsAvailable: 'Aucune option disponible pour le groupe de dépenses sélectionné.', }, filtersHeader: 'Filtres', diff --git a/src/languages/it.ts b/src/languages/it.ts index cd02bf1f54798..e9b0d28e2edda 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6296,6 +6296,7 @@ ${amount} per ${merchant} - ${date}`, delete: 'Elimina', hold: 'Attendere', unhold: 'Rimuovi blocco', + reject: 'Rifiuta', noOptionsAvailable: 'Nessuna opzione disponibile per il gruppo di spese selezionato.', }, filtersHeader: 'Filtri', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 15aad93fb0906..6af9909d8e2da 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6232,6 +6232,7 @@ ${date} - ${merchant}に${amount}`, delete: '削除', hold: '保留', unhold: '保留を解除', + reject: '却下', noOptionsAvailable: '選択した経費グループには利用可能なオプションがありません。', }, filtersHeader: 'フィルター', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 18f0d3a051e6e..1963cd0ce1c85 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6279,6 +6279,7 @@ ${amount} voor ${merchant} - ${date}`, delete: 'Verwijderen', hold: 'Vasthouden', unhold: 'Verwijder blokkering', + reject: 'Afwijzen', noOptionsAvailable: 'Geen opties beschikbaar voor de geselecteerde groep uitgaven.', }, filtersHeader: 'Filters', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 44522021bfac4..fdb7cc03e5ea5 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6266,6 +6266,7 @@ ${amount} dla ${merchant} - ${date}`, delete: 'Usuń', hold: 'Trzymaj', unhold: 'Usuń blokadę', + reject: 'Odrzuć', noOptionsAvailable: 'Brak dostępnych opcji dla wybranej grupy wydatków.', }, filtersHeader: 'Filtry', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1095f17d0c63d..c5421d0c9a8f2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6279,6 +6279,7 @@ ${amount} para ${merchant} - ${date}`, delete: 'Excluir', hold: 'Manter', unhold: 'Remover retenção', + reject: 'Rejeitar', noOptionsAvailable: 'Nenhuma opção disponível para o grupo de despesas selecionado.', }, filtersHeader: 'Filtros', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 06b42793ed867..042cbc7e2cc3a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6140,6 +6140,7 @@ ${merchant}的${amount} - ${date}`, delete: '删除', hold: '保持', unhold: '移除保留', + reject: '拒绝', noOptionsAvailable: '所选费用组没有可用选项。', }, filtersHeader: '筛选器', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8f829ffd913d7..8ffde61c5ddbf 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -70,9 +70,15 @@ const OPTIONS_PER_SCREEN: Partial [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: { animation: Animations.NONE, }, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS]: { + animation: Animations.NONE, + }, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: { animation: Animations.NONE, }, + [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: { + animation: Animations.NONE, + }, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: { animation: Animations.NONE, }, @@ -845,7 +851,9 @@ const SearchReportModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchRootVerifyAccountPage').default, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchMoneyRequestReportVerifyAccountPage').default, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS]: () => require('../../../../pages/Search/SearchRejectReasonPage').default, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, + [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: () => require('../../../../pages/Search/SearchRejectReasonPage').default, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default, }); diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index a733606877875..05caeb60b3de7 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -11,6 +11,7 @@ const SEARCH_TO_RHP: Partial['config'] = { [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, + [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: ROUTES.SEARCH_REJECT_REASON_RHP, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7606a14f4668f..86cc704ad1bc9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2602,6 +2602,11 @@ type SearchReportParamList = { /** Selected transactions' report ID */ reportID: string; }; + [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: Record; + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS]: { + /** Selected transactions' report ID */ + reportID: string; + }; }; type SearchFullscreenNavigatorParamList = { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9e707a324e1dc..d4d74de0c9fb1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12719,9 +12719,11 @@ function dismissRejectUseExplanation() { * @param transactionID - The ID of the transaction to reject * @param reportID - The ID of the expense report to reject * @param comment - The comment to add to the reject action - * @returns The route to navigate back to + * @param options + * - sharedRejectedToReportID: When rejecting multiple expenses sequentially, pass a single shared destination reportID so all rejections land in the same new report. + * @returns The route to navigate back to* */ -function rejectMoneyRequest(transactionID: string, reportID: string, comment: string): Route | undefined { +function rejectMoneyRequest(transactionID: string, reportID: string, comment: string, options?: {sharedRejectedToReportID?: string}): Route | undefined { const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const transactionAmount = getAmount(transaction); const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; @@ -12743,7 +12745,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`]; let movedToReport; - let rejectedToReportID; + let rejectedToReportID = options?.sharedRejectedToReportID; let urlToNavigateBack; let reportPreviewAction: OnyxTypes.ReportAction | undefined; let createdIOUReportActionID; @@ -13001,8 +13003,10 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st }, ); } else { - // Create optimistic report for the rejected transaction - rejectedToReportID = generateReportID(); + // When no existing open report is found, use the sharedRejectedToReportID + // so multiple sequential rejections land in the same destination report + // Fallback to generating a fresh ID if not provided + rejectedToReportID = rejectedToReportID ?? generateReportID(); const newExpenseReport = buildOptimisticExpenseReport( report.chatReportID, report?.policyID, @@ -13375,6 +13379,48 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st }); }); + // Add snapshot updates if called from the Reports page + const currentSearchQueryJSON = getCurrentSearchQueryJSON(); + if (currentSearchQueryJSON?.hash) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + isActionLoading: true, + errors: null, + }, + }, + }, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + isActionLoading: false, + }, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + isActionLoading: false, + errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }); + } + // Build API parameters const parameters: RejectMoneyRequestParams = { transactionID, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f9a897a18b26f..97b05b4e30022 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -26,6 +26,7 @@ import type {OptimisticExportIntegrationAction} from '@libs/ReportUtils'; import { buildOptimisticExportIntegrationAction, buildOptimisticIOUReportAction, + generateReportID, getReportTransactions, hasHeldExpenses, isExpenseReport, @@ -48,6 +49,7 @@ import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResult import type Nullable from '@src/types/utils/Nullable'; import SafeString from '@src/utils/SafeString'; import {setPersonalBankAccountContinueKYCOnSuccess} from './BankAccounts'; +import {rejectMoneyRequest} from './IOU'; import {setOptimisticTransactionThread} from './Report'; import {saveLastSearchParams} from './ReportNavigation'; @@ -593,6 +595,29 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } +function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string) { + const transactionIDs = Object.keys(selectedTransactions); + + const transactionsByReport: Record = {}; + transactionIDs.forEach((transactionID) => { + const reportID = selectedTransactions[transactionID].reportID; + if (!transactionsByReport[reportID]) { + transactionsByReport[reportID] = []; + } + transactionsByReport[reportID].push(transactionID); + }); + + Object.entries(transactionsByReport).forEach(([reportID, reportTransactionIDs]) => { + // Share a single destination ID across all rejections from the same source report + const sharedRejectedToReportID = generateReportID(); + reportTransactionIDs.forEach((transactionID) => { + rejectMoneyRequest(transactionID, reportID, comment, {sharedRejectedToReportID}); + }); + }); + + playSound(SOUNDS.SUCCESS); +} + function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { const {optimisticData: loadingOptimisticData, finallyData} = getOnyxLoadingData(hash); // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 @@ -993,6 +1018,7 @@ export { deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, + rejectMoneyRequestsOnSearch, exportSearchItemsToCSV, queueExportSearchItemsToCSV, queueExportSearchWithTemplate, diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 50abf63ab9ed3..ae4a659dee5c5 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -44,11 +44,6 @@ function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { 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], diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 057e98b96149c..64d77a83635e5 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -68,6 +68,7 @@ import { isBusinessInvoiceRoom, isExpenseReport as isExpenseReportUtil, isInvoiceReport, + canRejectReportAction, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; @@ -289,6 +290,7 @@ function SearchPage({route}: SearchPageProps) { ) as PaymentData[]; payMoneyRequestOnSearch(hash, paymentData, transactionIDList); + // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { clearSelectedTransactions(); diff --git a/src/pages/Search/SearchRejectReasonPage.tsx b/src/pages/Search/SearchRejectReasonPage.tsx new file mode 100644 index 0000000000000..03ae7fd3938af --- /dev/null +++ b/src/pages/Search/SearchRejectReasonPage.tsx @@ -0,0 +1,49 @@ +import React, {useCallback, useEffect} from 'react'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import {useSearchContext} from '@components/Search/SearchContext'; +import useLocalize from '@hooks/useLocalize'; +import {clearErrorFields, clearErrors} from '@libs/actions/FormActions'; +import {rejectMoneyRequestsOnSearch} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; +import RejectReasonFormView from '@pages/iou/RejectReasonFormView'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/MoneyRequestRejectReasonForm'; + +function SearchRejectReasonPage() { + const {translate} = useLocalize(); + const context = useSearchContext(); + + const onSubmit = useCallback( + ({comment}: FormOnyxValues) => { + rejectMoneyRequestsOnSearch(context.currentSearchHash, context.selectedTransactions, comment); + context.clearSelectedTransactions(); + Navigation.goBack(); + }, + [context], + ); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]); + return errors; + }, + [translate], + ); + + useEffect(() => { + clearErrors(ONYXKEYS.FORMS.MONEY_REQUEST_REJECT_FORM); + clearErrorFields(ONYXKEYS.FORMS.MONEY_REQUEST_REJECT_FORM); + }, []); + + return ( + + ); +} + +SearchRejectReasonPage.displayName = 'SearchRejectReasonPage'; + +export default SearchRejectReasonPage; diff --git a/src/pages/iou/RejectReasonFormView.tsx b/src/pages/iou/RejectReasonFormView.tsx index 7b1a0da7ba7e5..3e28b7edd628a 100644 --- a/src/pages/iou/RejectReasonFormView.tsx +++ b/src/pages/iou/RejectReasonFormView.tsx @@ -23,7 +23,7 @@ type RejectReasonFormViewProps = { validate: (values: FormOnyxValues) => Partial>; /** Link to previous page */ - backTo: Route; + backTo?: Route; }; function RejectReasonFormView({backTo, validate, onSubmit}: RejectReasonFormViewProps) { From 00d2bebf74838f9426f4abcf620a4a48a77fd65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 14:45:42 +0100 Subject: [PATCH 02/30] fix pay button showing --- src/libs/actions/Search.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 97b05b4e30022..b5973959c8ab6 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -598,6 +598,18 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string) { const transactionIDs = Object.keys(selectedTransactions); + Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, { + data: Object.fromEntries( + transactionIDs.map((transactionID) => [ + `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + { + action: CONST.SEARCH.ACTION_TYPES.VIEW, + allActions: [CONST.SEARCH.ACTION_TYPES.VIEW], + }, + ]), + ) as Partial, + }); + const transactionsByReport: Record = {}; transactionIDs.forEach((transactionID) => { const reportID = selectedTransactions[transactionID].reportID; From fbf83d73a157a6f27acb177805ed84651d841097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 14:46:12 +0100 Subject: [PATCH 03/30] fix held expenses and already rejected transactions --- src/pages/Search/SearchPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 64d77a83635e5..086b8557c8085 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -85,6 +85,7 @@ import type SCREENS from '@src/SCREENS'; import type {SearchResults, Transaction} from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; import SearchPageNarrow from './SearchPageNarrow'; +import { getTransactionViolationsOfTransaction } from '@libs/TransactionUtils'; type SearchPageProps = PlatformStackScreenProps; From 9215e10380e51727d4efe22919ccde15dce80a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 15:43:24 +0100 Subject: [PATCH 04/30] add `checkBulkRejectHydration()` --- src/libs/ReportUtils.ts | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bcfd803b05e4f..2e6eab8ec40f0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12484,10 +12484,58 @@ function getReportURLForCurrentContext(reportID: string | undefined): string { return `${environmentURL}/${relativePath}`; } +/** + * Checks if all selected items have the necessary Onyx data hydrated for bulk rejection. + */ +function checkBulkRejectHydration( + selectedTransactions: Record, + policies: Record | null | undefined, +): {areHydrated: boolean; missingReportIDs: string[]; missingPolicyIDs: string[]} { + const transactionIDs = Object.keys(selectedTransactions); + const missingReportIDs: string[] = []; + const missingPolicyIDs: string[] = []; + + for (const transactionID of transactionIDs) { + const transaction = selectedTransactions[transactionID]; + const reportID = transaction?.reportID; + + // Check if we have the report data + const report = getReportOrDraftReport(reportID); + if (!report) { + missingReportIDs.push(reportID); + continue; + } + + const effectivePolicyID = transaction?.policyID ?? report.policyID; + + // Check if we have the policy data (required for canRejectReportAction check) + if (effectivePolicyID) { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${effectivePolicyID}`]; + if (!policy) { + missingPolicyIDs.push(effectivePolicyID); + continue; + } + } + + // Check if essential report fields are present + if (report.managerID == null || report.stateNum == null || report.statusNum == null) { + missingReportIDs.push(reportID); + continue; + } + } + + return { + areHydrated: missingReportIDs.length === 0 && missingPolicyIDs.length === 0, + missingReportIDs: [...new Set(missingReportIDs)], + missingPolicyIDs: [...new Set(missingPolicyIDs)], + }; +} + export { areAllRequestsBeingSmartScanned, buildOptimisticAddCommentReportAction, buildOptimisticApprovedReportAction, + checkBulkRejectHydration, buildOptimisticUnapprovedReportAction, buildOptimisticCancelPaymentReportAction, buildOptimisticChangedTaskAssigneeReportAction, From 2b9de1f5bd73607b2ca9e27d92fa8c0f6d47ffe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 15:43:43 +0100 Subject: [PATCH 05/30] fix hydration for Reject --- src/pages/Search/SearchPage.tsx | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 086b8557c8085..6d93d064ea9ef 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -35,7 +35,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {confirmReadyToOpenApp} from '@libs/actions/App'; -import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; +import {openWorkspace} from '@libs/actions/Policy/Policy'; +import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, openReport, searchInServer} from '@libs/actions/Report'; import { approveMoneyRequestOnSearch, deleteMoneyRequestOnSearch, @@ -62,13 +63,14 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {getActiveAdminWorkspaces, hasDynamicExternalWorkflow, hasVBBA, isPaidGroupPolicy} from '@libs/PolicyUtils'; import { + canRejectReportAction, + checkBulkRejectHydration, generateReportID, getPolicyExpenseChat, getReportOrDraftReport, isBusinessInvoiceRoom, isExpenseReport as isExpenseReportUtil, isInvoiceReport, - canRejectReportAction, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; @@ -169,6 +171,48 @@ function SearchPage({route}: SearchPageProps) { } }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults]); + // Check hydration status and prefetch missing data for bulk reject + const bulkRejectHydrationStatus = useMemo(() => { + if (Object.keys(selectedTransactions).length === 0) { + return {areHydrated: true, missingReportIDs: [], missingPolicyIDs: []}; + } + return checkBulkRejectHydration(selectedTransactions, policies); + }, [selectedTransactions, policies]); + + // Prefetch missing report and policy data when items are selected + const lastPrefetchKeyRef = useRef(''); + useEffect(() => { + const {areHydrated, missingReportIDs, missingPolicyIDs} = bulkRejectHydrationStatus; + + // If hydrated or nothing selected, clear and exit + if (areHydrated) { + lastPrefetchKeyRef.current = ''; + return; + } + if (isOffline) { + return; + } + + const key = `${[...missingReportIDs].sort().join(',')}|${[...missingPolicyIDs].sort().join(',')}`; + if (key === lastPrefetchKeyRef.current) { + return; + } + + // Prefetch once per unique set of missing IDs + missingReportIDs.forEach((id) => id && openReport(id)); + missingPolicyIDs.forEach((id) => id && openWorkspace(id, [])); + + lastPrefetchKeyRef.current = key; + }, [bulkRejectHydrationStatus, isOffline]); + + // Allow retry on reconnect + const prevIsOffline = usePrevious(isOffline); + useEffect(() => { + if (prevIsOffline && !isOffline) { + lastPrefetchKeyRef.current = ''; + } + }, [isOffline, prevIsOffline]); + const {status, hash} = queryJSON ?? {}; const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); @@ -450,6 +494,7 @@ function SearchPage({route}: SearchPageProps) { text: translate('common.submit'), value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, shouldCloseModalOnSelect: true, + disabled: isRejectDisabled, onSelected: () => { if (isOffline) { setIsOfflineModalVisible(true); @@ -643,6 +688,7 @@ function SearchPage({route}: SearchPageProps) { selectedReportIDs, selectedTransactionReportIDs, isBetaBulkPayEnabled, + bulkRejectHydrationStatus, ]); const handleDeleteExpenses = () => { From 2b297ff125dd006df3cae07b26553021780cc7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 31 Oct 2025 17:12:15 +0100 Subject: [PATCH 06/30] remove logic that doesn't work for multi expenses --- src/libs/actions/Search.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b5973959c8ab6..97b05b4e30022 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -598,18 +598,6 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string) { const transactionIDs = Object.keys(selectedTransactions); - Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, { - data: Object.fromEntries( - transactionIDs.map((transactionID) => [ - `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - { - action: CONST.SEARCH.ACTION_TYPES.VIEW, - allActions: [CONST.SEARCH.ACTION_TYPES.VIEW], - }, - ]), - ) as Partial, - }); - const transactionsByReport: Record = {}; transactionIDs.forEach((transactionID) => { const reportID = selectedTransactions[transactionID].reportID; From c570d367284a88b224b067df71ad7ff9db1dd060 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 02:00:27 +0700 Subject: [PATCH 07/30] fix: restore missing logic --- src/pages/Search/SearchPage.tsx | 55 ++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 6d93d064ea9ef..f3e803fb93c2e 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -75,6 +75,7 @@ import { } from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {getTransactionViolationsOfTransaction} from '@libs/TransactionUtils'; import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types'; import variables from '@styles/variables'; import {initMoneyRequest, setMoneyRequestParticipantsFromReport, setMoneyRequestReceipt} from '@userActions/IOU'; @@ -87,7 +88,6 @@ import type SCREENS from '@src/SCREENS'; import type {SearchResults, Transaction} from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; import SearchPageNarrow from './SearchPageNarrow'; -import { getTransactionViolationsOfTransaction } from '@libs/TransactionUtils'; type SearchPageProps = PlatformStackScreenProps; @@ -482,6 +482,59 @@ function SearchPage({route}: SearchPageProps) { }); } + // Check if any items are explicitly not rejectable (only when data is hydrated) + // If data is not hydrated, we don't treat it as "not rejectable" - instead we'll disable the button + const login = currentUserPersonalDetails?.login ?? ''; + const areAnyExplicitlyNotRejectable = + selectedTransactionReportIDs.length > 0 && + selectedTransactionReportIDs.some((id) => { + const report = getReportOrDraftReport(id); + if (!report) { + return false; + } + const policyForReport = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + if (!policyForReport) { + return false; + } + return !canRejectReportAction(login, report, policyForReport); + }); + + const hasNoRejectedTransaction = selectedTransactionsKeys.every((id) => { + const transactionViolations = getTransactionViolationsOfTransaction(id) ?? []; + return !transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE); + }); + + // Check if all selected items have hydrated Onyx data for rejection + const {areHydrated: areItemsHydratedForReject} = bulkRejectHydrationStatus; + + // Show the Reject option unless we know for sure it's not allowed + const shouldShowRejectOption = !isOffline && !areAnyExplicitlyNotRejectable && hasNoRejectedTransaction; + + // Disabled if not hydrated + const isRejectDisabled = !areItemsHydratedForReject; + + if (shouldShowRejectOption) { + options.push({ + icon: Expensicons.ThumbsDown, + text: translate('search.bulkActions.reject'), + value: CONST.SEARCH.BULK_ACTION_TYPES.REJECT, + shouldCloseModalOnSelect: true, + disabled: isRejectDisabled, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (!areItemsHydratedForReject) { + return; + } + + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + }, + }); + } + const shouldShowSubmitOption = !isOffline && (selectedReports.length From 5c90caf3f391c47fb48d59e2167d89d6195d8959 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 08:58:25 +0700 Subject: [PATCH 08/30] fix: education modal isn't shown --- src/pages/Search/SearchPage.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index f3e803fb93c2e..08bcf071a8acb 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -8,6 +8,7 @@ import DecisionModal from '@components/DecisionModal'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; +import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PaymentMethodType} from '@components/KYCWall/types'; import type {PopoverMenuItem} from '@components/PopoverMenu'; @@ -78,7 +79,7 @@ import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {getTransactionViolationsOfTransaction} from '@libs/TransactionUtils'; import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types'; import variables from '@styles/variables'; -import {initMoneyRequest, setMoneyRequestParticipantsFromReport, setMoneyRequestReceipt} from '@userActions/IOU'; +import {dismissRejectUseExplanation, initMoneyRequest, setMoneyRequestParticipantsFromReport, setMoneyRequestReceipt} from '@userActions/IOU'; import {openOldDotLink} from '@userActions/Link'; import {buildOptimisticTransactionAndCreateDraft} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; @@ -121,6 +122,13 @@ function SearchPage({route}: SearchPageProps) { const [isExportWithTemplateModalVisible, setIsExportWithTemplateModalVisible] = useState(false); const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); const [isDEWModalVisible, setIsDEWModalVisible] = useState(false); + const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION, {canBeMissing: true}); + const [isRejectEducationalModalVisible, setIsRejectEducationalModalVisible] = useState(false); + const dismissModalAndUpdateUseReject = () => { + setIsRejectEducationalModalVisible(false); + dismissRejectUseExplanation(); + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + }; const queryJSON = useMemo(() => buildSearchQueryJSON(route.params.q), [route.params.q]); const {saveScrollOffset} = useContext(ScrollOffsetContext); const activeAdminPolicies = getActiveAdminWorkspaces(policies, currentUserPersonalDetails?.accountID.toString()).sort((a, b) => localeCompare(a.name || '', b.name || '')); @@ -530,7 +538,11 @@ function SearchPage({route}: SearchPageProps) { return; } - Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + if (dismissedRejectUseExplanation) { + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + } else { + setIsRejectEducationalModalVisible(true); + } }, }); } @@ -1153,6 +1165,12 @@ function SearchPage({route}: SearchPageProps) { confirmText={translate('customApprovalWorkflow.goToExpensifyClassic')} shouldShowCancelButton={false} /> + {!!isRejectEducationalModalVisible && ( + + )} ); From 5259dd2ebd9653b57be2a542679c89f8e54ee0fb Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 09:49:40 +0700 Subject: [PATCH 09/30] fix: add new bulk reject API --- src/libs/API/types.ts | 1 + src/libs/actions/Search.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e63ab634418a9..c4727c705daec 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -351,6 +351,7 @@ const WRITE_COMMANDS = { UPGRADE_TO_CORPORATE: 'UpgradeToCorporate', DOWNGRADE_TO_TEAM: 'Policy_DowngradeToTeam', DELETE_MONEY_REQUEST_ON_SEARCH: 'DeleteMoneyRequestOnSearch', + REJECT_MONEY_REQUEST_IN_BULK: 'RejectMoneyRequestInBulk', HOLD_MONEY_REQUEST_ON_SEARCH: 'HoldMoneyRequestOnSearch', APPROVE_MONEY_REQUEST_ON_SEARCH: 'ApproveMoneyRequestOnSearch', UNHOLD_MONEY_REQUEST_ON_SEARCH: 'UnholdMoneyRequestOnSearch', diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 97b05b4e30022..128e95f890315 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -595,6 +595,12 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } +function rejectMoneyRequestOnSearch(hash: number, reportID: string, comment: string){ + const {optimisticData, finallyData} = getOnyxLoadingData(hash); + + API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment}, {optimisticData, finallyData}); +} + function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string) { const transactionIDs = Object.keys(selectedTransactions); From bd85a3500839a23e7b6dd5cac66b223290545321 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 17:13:37 +0700 Subject: [PATCH 10/30] fix: update bulk reject logic --- src/libs/actions/Search.ts | 30 ++++++++++++++------- src/pages/Search/SearchRejectReasonPage.tsx | 8 +++--- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 128e95f890315..6c27454546164 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -20,7 +20,7 @@ import Navigation from '@libs/Navigation/Navigation'; import enhanceParameters from '@libs/Network/enhanceParameters'; import {rand64} from '@libs/NumberUtils'; import {getActivePaymentType} from '@libs/PaymentUtils'; -import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; +import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration, hasDynamicExternalWorkflow, isDelayedSubmissionEnabled} from '@libs/PolicyUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import type {OptimisticExportIntegrationAction} from '@libs/ReportUtils'; import { @@ -42,7 +42,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; -import type {ExportTemplate, LastPaymentMethod, LastPaymentMethodType, Policy, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import type {ExportTemplate, LastPaymentMethod, LastPaymentMethodType, Policy, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; @@ -595,13 +595,13 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } -function rejectMoneyRequestOnSearch(hash: number, reportID: string, comment: string){ +function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: string) { const {optimisticData, finallyData} = getOnyxLoadingData(hash); API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment}, {optimisticData, finallyData}); } -function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string) { +function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { const transactionIDs = Object.keys(selectedTransactions); const transactionsByReport: Record = {}; @@ -613,12 +613,22 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte transactionsByReport[reportID].push(transactionID); }); - Object.entries(transactionsByReport).forEach(([reportID, reportTransactionIDs]) => { - // Share a single destination ID across all rejections from the same source report - const sharedRejectedToReportID = generateReportID(); - reportTransactionIDs.forEach((transactionID) => { - rejectMoneyRequest(transactionID, reportID, comment, {sharedRejectedToReportID}); - }); + Object.entries(transactionsByReport).forEach(([reportID, selectedTransactionIDs]) => { + const allReportTransactions = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const allTransactionIDs = allReportTransactions.map((transaction) => transaction.transactionID); + const areAllExpensesSelected = allTransactionIDs.every((transactionID) => selectedTransactionIDs.includes(transactionID)); + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const isPolicyDelayedSubmissionEnabled = policy ? isDelayedSubmissionEnabled(policy) : false; + if (isPolicyDelayedSubmissionEnabled && areAllExpensesSelected) { + rejectMoneyRequestInBulk(hash, reportID, comment); + } else { + // Share a single destination ID across all rejections from the same source report + const sharedRejectedToReportID = generateReportID(); + selectedTransactionIDs.forEach((transactionID) => { + rejectMoneyRequest(transactionID, reportID, comment, {sharedRejectedToReportID}); + }); + } }); playSound(SOUNDS.SUCCESS); diff --git a/src/pages/Search/SearchRejectReasonPage.tsx b/src/pages/Search/SearchRejectReasonPage.tsx index 03ae7fd3938af..fde9543bb02eb 100644 --- a/src/pages/Search/SearchRejectReasonPage.tsx +++ b/src/pages/Search/SearchRejectReasonPage.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useEffect} from 'react'; +import {useOnyx} from 'react-native-onyx'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; @@ -13,14 +14,15 @@ import INPUT_IDS from '@src/types/form/MoneyRequestRejectReasonForm'; function SearchRejectReasonPage() { const {translate} = useLocalize(); const context = useSearchContext(); - + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const onSubmit = useCallback( ({comment}: FormOnyxValues) => { - rejectMoneyRequestsOnSearch(context.currentSearchHash, context.selectedTransactions, comment); + rejectMoneyRequestsOnSearch(context.currentSearchHash, context.selectedTransactions, comment, allPolicies, allReports); context.clearSelectedTransactions(); Navigation.goBack(); }, - [context], + [context, allPolicies, allReports], ); const validate = useCallback( From 475a378a2778db96d328cd87db4082bc2e12b34a Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 17:27:45 +0700 Subject: [PATCH 11/30] fix: lint --- src/pages/Search/SearchPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 08bcf071a8acb..7f375d832793f 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -216,9 +216,10 @@ function SearchPage({route}: SearchPageProps) { // Allow retry on reconnect const prevIsOffline = usePrevious(isOffline); useEffect(() => { - if (prevIsOffline && !isOffline) { - lastPrefetchKeyRef.current = ''; + if (isOffline || !prevIsOffline) { + return; } + lastPrefetchKeyRef.current = ''; }, [isOffline, prevIsOffline]); const {status, hash} = queryJSON ?? {}; From 9175e78c58bcc3f06ec523b7fd9bfa488a3fb8fa Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 17:42:17 +0700 Subject: [PATCH 12/30] fix: type --- src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 1 + 3 files changed, 8 insertions(+) create mode 100644 src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts diff --git a/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts b/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts new file mode 100644 index 0000000000000..5a8eb07ae8b36 --- /dev/null +++ b/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts @@ -0,0 +1,6 @@ +type RejectMoneyRequestInBulkParams = { + reportID: string; + comment: string; +}; + +export default RejectMoneyRequestInBulkParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 63fac4774afa3..2ec41ec419b0e 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -287,6 +287,7 @@ export type {default as MarkAsExportedParams} from './MarkAsExportedParams'; export type {default as UpgradeToCorporateParams} from './UpgradeToCorporateParams'; export type {default as DowngradeToTeamParams} from './DowngradeToTeamParams'; export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyRequestOnSearchParams'; +export type {default as RejectMoneyRequestInBulkParams} from './RejectMoneyRequestInBulkParams'; export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams'; export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams'; export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c4727c705daec..76f893840cefd 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -895,6 +895,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SEND_SCHEDULE_CALL_NUDGE]: Parameters.SendScheduleCallNudgeParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; + [WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK]: Parameters.RejectMoneyRequestInBulkParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; [WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams; [WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.UnholdMoneyRequestOnSearchParams; From 3809cfe0a25a3fbaf57ba71d6d74bf3316d1d4c7 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 13 Nov 2025 17:57:37 +0700 Subject: [PATCH 13/30] fix: rename variable to non negative --- src/pages/Search/SearchPage.tsx | 8 ++++---- src/pages/Search/SearchRejectReasonPage.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7f375d832793f..035a28e7d1a1a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -494,9 +494,9 @@ function SearchPage({route}: SearchPageProps) { // Check if any items are explicitly not rejectable (only when data is hydrated) // If data is not hydrated, we don't treat it as "not rejectable" - instead we'll disable the button const login = currentUserPersonalDetails?.login ?? ''; - const areAnyExplicitlyNotRejectable = + const areAllExplicitlyRejectable = selectedTransactionReportIDs.length > 0 && - selectedTransactionReportIDs.some((id) => { + selectedTransactionReportIDs.every((id) => { const report = getReportOrDraftReport(id); if (!report) { return false; @@ -505,7 +505,7 @@ function SearchPage({route}: SearchPageProps) { if (!policyForReport) { return false; } - return !canRejectReportAction(login, report, policyForReport); + return canRejectReportAction(login, report, policyForReport); }); const hasNoRejectedTransaction = selectedTransactionsKeys.every((id) => { @@ -517,7 +517,7 @@ function SearchPage({route}: SearchPageProps) { const {areHydrated: areItemsHydratedForReject} = bulkRejectHydrationStatus; // Show the Reject option unless we know for sure it's not allowed - const shouldShowRejectOption = !isOffline && !areAnyExplicitlyNotRejectable && hasNoRejectedTransaction; + const shouldShowRejectOption = !isOffline && areAllExplicitlyRejectable && hasNoRejectedTransaction; // Disabled if not hydrated const isRejectDisabled = !areItemsHydratedForReject; diff --git a/src/pages/Search/SearchRejectReasonPage.tsx b/src/pages/Search/SearchRejectReasonPage.tsx index fde9543bb02eb..c755144fcf1a9 100644 --- a/src/pages/Search/SearchRejectReasonPage.tsx +++ b/src/pages/Search/SearchRejectReasonPage.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useEffect} from 'react'; -import {useOnyx} from 'react-native-onyx'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import {clearErrorFields, clearErrors} from '@libs/actions/FormActions'; import {rejectMoneyRequestsOnSearch} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; From 4a31d379eea3649aa21490e879c55c10c737f6ae Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 20 Nov 2025 15:32:39 +0700 Subject: [PATCH 14/30] fix: type --- src/libs/actions/IOU.ts | 42 ----------------------------------------- 1 file changed, 42 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 82a4403e4288f..cb0bb8debe3c4 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -13565,48 +13565,6 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st }); } - // Add snapshot updates if called from the Reports page - const currentSearchQueryJSON = getCurrentSearchQueryJSON(); - if (currentSearchQueryJSON?.hash) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { - isActionLoading: true, - errors: null, - }, - }, - }, - }); - - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { - isActionLoading: false, - }, - }, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { - isActionLoading: false, - errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, - }, - }, - }); - } - // Build API parameters const parameters: RejectMoneyRequestParams = { transactionID, From 6dd1cd322216a9df63b36b42952be7f1c130604f Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 20 Nov 2025 16:08:11 +0700 Subject: [PATCH 15/30] fix: remove redundant change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index bc7dd9f534cf0..1d095f65ed1b5 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit bc7dd9f534cf01b8528b7f89fa308257edb05592 +Subproject commit 1d095f65ed1b5f056eafb8b71063e54446f7414b From f406e66d6bab8701b1b0973ccbfb77703c15a7b3 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 20 Nov 2025 17:18:40 +0700 Subject: [PATCH 16/30] fix: remove redundant changes --- src/pages/Search/SearchHoldReasonPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index ae4a659dee5c5..50abf63ab9ed3 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -44,6 +44,11 @@ function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { 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], From c56ae688a5e153422c2c700e43e624ae219cd2fe Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 21 Nov 2025 15:16:44 +0700 Subject: [PATCH 17/30] fix: remove redundant change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 1d095f65ed1b5..df39af2c57f2e 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 1d095f65ed1b5f056eafb8b71063e54446f7414b +Subproject commit df39af2c57f2e03b9ab19c50cb8d8436f67df591 From e1a469f38318ad1ca40d57c078b3c6035700c72d Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 21 Nov 2025 17:07:54 +0700 Subject: [PATCH 18/30] fix: handle optimistic data when bulk reject --- src/libs/actions/IOU.ts | 34 +++++++++++++++++++++++++++++----- src/libs/actions/Search.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4a6b81266d0c3..7b634927a094e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -317,6 +317,14 @@ type MoneyRequestInformation = { reimbursable?: boolean; }; +type RejectMoneyRequestData = { + optimisticData: OnyxUpdate[]; + successData: OnyxUpdate[]; + failureData: OnyxUpdate[]; + parameters: RejectMoneyRequestParams; + urlToNavigateBack: Route | undefined; +}; + type TrackExpenseInformation = { createdWorkspaceParams?: CreateWorkspaceParams; iouReport?: OnyxTypes.Report; @@ -12966,15 +12974,21 @@ function dismissRejectUseExplanation() { } /** - * Reject a money request + * Retrieve the reject money request data * @param transactionID - The ID of the transaction to reject * @param reportID - The ID of the expense report to reject * @param comment - The comment to add to the reject action * @param options * - sharedRejectedToReportID: When rejecting multiple expenses sequentially, pass a single shared destination reportID so all rejections land in the same new report. - * @returns The route to navigate back to* + * @returns optimisticData, successData, failureData, parameters, urlToNavigateBack */ -function rejectMoneyRequest(transactionID: string, reportID: string, comment: string, options?: {sharedRejectedToReportID?: string}): Route | undefined { +function prepareRejectMoneyRequestData( + transactionID: string, + reportID: string, + comment: string, + options?: {sharedRejectedToReportID?: string}, + shouldUseBulkAction?: boolean, +): RejectMoneyRequestData | undefined { const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const transactionAmount = getAmount(transaction); const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; @@ -13019,7 +13033,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; - if (!isPolicyDelayedSubmissionEnabled || isIOU) { + if ((!isPolicyDelayedSubmissionEnabled || isIOU) && !shouldUseBulkAction) { if (hasMultipleExpenses) { // For reports with multiple expenses: Update report total optimisticData.push( @@ -13158,7 +13172,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st urlToNavigateBack = ROUTES.REPORT_WITH_ID.getRoute(report.chatReportID); } } - } else if (hasMultipleExpenses) { + } else if (hasMultipleExpenses && !shouldUseBulkAction) { if (isUserOnSearchPage || isUserOnSearchMoneyRequestReport) { // Navigate to the existing Reports > Expense view. urlToNavigateBack = undefined; @@ -13644,6 +13658,15 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st expenseCreatedReportActionID, }; + return {optimisticData, successData, failureData, parameters, urlToNavigateBack: urlToNavigateBack as Route}; +} + +function rejectMoneyRequest(transactionID: string, reportID: string, comment: string, options?: {sharedRejectedToReportID?: string}): Route | undefined { + const data = prepareRejectMoneyRequestData(transactionID, reportID, comment, options); + if (!data) { + return; + } + const {urlToNavigateBack, optimisticData, successData, failureData, parameters} = data; // Make API call API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); @@ -14729,6 +14752,7 @@ export { calculateDiffAmount, dismissRejectUseExplanation, rejectMoneyRequest, + prepareRejectMoneyRequestData, markRejectViolationAsResolved, setMoneyRequestReimbursable, computePerDiemExpenseAmount, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b3a3ee356ba1f..3dd0e49ad94c9 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -50,7 +50,7 @@ import type SearchResults from '@src/types/onyx/SearchResults'; import type Nullable from '@src/types/utils/Nullable'; import SafeString from '@src/utils/SafeString'; import {setPersonalBankAccountContinueKYCOnSuccess} from './BankAccounts'; -import {rejectMoneyRequest} from './IOU'; +import {prepareRejectMoneyRequestData, rejectMoneyRequest} from './IOU'; import {setOptimisticTransactionThread} from './Report'; import {saveLastSearchParams} from './ReportNavigation'; @@ -700,10 +700,31 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } -function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: string) { +function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: string, transactionIDs: string[]) { const {optimisticData, finallyData} = getOnyxLoadingData(hash); + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + const transactionIDToRejectReportAction: Record< + string, + { + rejectedActionReportActionID: string; + rejectedCommentReportActionID: string; + } + > = {}; + transactionIDs.forEach((transactionID) => { + const data = prepareRejectMoneyRequestData(transactionID, reportID, comment, undefined, true); + if (data) { + optimisticData.push(...data.optimisticData); + successData.push(...data.successData); + failureData.push(...data.failureData); + transactionIDToRejectReportAction[transactionID] = { + rejectedActionReportActionID: data.parameters.rejectedActionReportActionID, + rejectedCommentReportActionID: data.parameters.rejectedCommentReportActionID, + }; + } + }); - API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment}, {optimisticData, finallyData}); + API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment}, {optimisticData, successData, failureData, finallyData}); } function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { @@ -726,7 +747,7 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const isPolicyDelayedSubmissionEnabled = policy ? isDelayedSubmissionEnabled(policy) : false; if (isPolicyDelayedSubmissionEnabled && areAllExpensesSelected) { - rejectMoneyRequestInBulk(hash, reportID, comment); + rejectMoneyRequestInBulk(hash, reportID, comment, allTransactionIDs); } else { // Share a single destination ID across all rejections from the same source report const sharedRejectedToReportID = generateReportID(); From 97769b642327c022123a6e94c5344ba6ae2c33c8 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 10:18:50 +0700 Subject: [PATCH 19/30] fix: lint --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 381a31117aa8d..301c7f83fe12e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -13670,7 +13670,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st // Make API call API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); - return urlToNavigateBack as Route; + return urlToNavigateBack!; } function markRejectViolationAsResolved(transactionID: string, reportID?: string) { From c45bf2a2961d84125d1e1b841f4cc5998a46950e Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 10:39:22 +0700 Subject: [PATCH 20/30] fix: lint --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 301c7f83fe12e..a264621a8e9e3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -13670,7 +13670,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st // Make API call API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); - return urlToNavigateBack!; + return urlToNavigateBack; } function markRejectViolationAsResolved(transactionID: string, reportID?: string) { From a1edc60f580db6576178a11253d210e06218dcff Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 10:44:12 +0700 Subject: [PATCH 21/30] fix: remove sound when reject --- src/libs/actions/Search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a45f02424578a..7a6e2f92e7af7 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -755,8 +755,6 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte }); } }); - - playSound(SOUNDS.SUCCESS); } function deleteMoneyRequestOnSearch( From 728aecc61e26316404a467bd39eec021ff109483 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 10:53:26 +0700 Subject: [PATCH 22/30] fix: use reduce instead of foreach --- src/libs/actions/Search.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 7a6e2f92e7af7..a4341b05191ef 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -729,14 +729,17 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { const transactionIDs = Object.keys(selectedTransactions); - const transactionsByReport: Record = {}; - transactionIDs.forEach((transactionID) => { + const transactionsByReport = transactionIDs.reduce>((acc, transactionID) => { const reportID = selectedTransactions[transactionID].reportID; - if (!transactionsByReport[reportID]) { - transactionsByReport[reportID] = []; + + if (!acc[reportID]) { + acc[reportID] = []; } - transactionsByReport[reportID].push(transactionID); - }); + + acc[reportID].push(transactionID); + + return acc; + }, {}); Object.entries(transactionsByReport).forEach(([reportID, selectedTransactionIDs]) => { const allReportTransactions = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); From 782f69d3ff3554d05517b8287193cd22e4259bab Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 11:13:56 +0700 Subject: [PATCH 23/30] fix: missing education modal in small screen --- src/pages/Search/SearchPage.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 93dd9c284a8a7..639b4d7dff676 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1008,6 +1008,12 @@ function SearchPage({route}: SearchPageProps) { confirmText={translate('customApprovalWorkflow.goToExpensifyClassic')} shouldShowCancelButton={false} /> + {!!isRejectEducationalModalVisible && ( + + )} )} From 142ef8532022077e7636ebbc01958553eb955179 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 24 Nov 2025 16:59:55 +0700 Subject: [PATCH 24/30] fix: remove disabled prop in submit option --- src/pages/Search/SearchPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 639b4d7dff676..e6010a88b4fcb 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -571,7 +571,6 @@ function SearchPage({route}: SearchPageProps) { text: translate('common.submit'), value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, shouldCloseModalOnSelect: true, - disabled: isRejectDisabled, onSelected: () => { if (isOffline) { setIsOfflineModalVisible(true); From 76462394b246b6997240d8b2959d8ae3da9d65fd Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 25 Nov 2025 04:49:28 +0700 Subject: [PATCH 25/30] fix: lint --- .../API/parameters/RejectMoneyRequestInBulkParams.ts | 1 + src/libs/actions/Search.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts b/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts index 5a8eb07ae8b36..ca25c749e23b7 100644 --- a/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts +++ b/src/libs/API/parameters/RejectMoneyRequestInBulkParams.ts @@ -1,6 +1,7 @@ type RejectMoneyRequestInBulkParams = { reportID: string; comment: string; + transactionIDToRejectReportAction: string; }; export default RejectMoneyRequestInBulkParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f1f0567eea205..116246b9d1fb5 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -706,8 +706,8 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin const transactionIDToRejectReportAction: Record< string, { - rejectedActionReportActionID: string; - rejectedCommentReportActionID: string; + rejectedActionReportActionID: number; + rejectedCommentReportActionID: number; } > = {}; transactionIDs.forEach((transactionID) => { @@ -717,13 +717,13 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin successData.push(...data.successData); failureData.push(...data.failureData); transactionIDToRejectReportAction[transactionID] = { - rejectedActionReportActionID: data.parameters.rejectedActionReportActionID, - rejectedCommentReportActionID: data.parameters.rejectedCommentReportActionID, + rejectedActionReportActionID: Number(data.parameters.rejectedActionReportActionID), + rejectedCommentReportActionID: Number(data.parameters.rejectedCommentReportActionID), }; } }); - API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment}, {optimisticData, successData, failureData, finallyData}); + API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment, transactionIDToRejectReportAction: JSON.stringify(transactionIDToRejectReportAction)}, {optimisticData, successData, failureData, finallyData}); } function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { From 9e04cc00e4a4479d81c3f3b0946d38c42f48ded5 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 25 Nov 2025 04:59:28 +0700 Subject: [PATCH 26/30] fix: prettier --- src/libs/actions/Search.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 116246b9d1fb5..a6c79e83dbb20 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -723,7 +723,11 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin } }); - API.write(WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, {reportID, comment, transactionIDToRejectReportAction: JSON.stringify(transactionIDToRejectReportAction)}, {optimisticData, successData, failureData, finallyData}); + API.write( + WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, + {reportID, comment, transactionIDToRejectReportAction: JSON.stringify(transactionIDToRejectReportAction)}, + {optimisticData, successData, failureData, finallyData}, + ); } function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { From da3dcbe0b926e62f14cfff6660a89244b989b98c Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 25 Nov 2025 05:00:31 +0700 Subject: [PATCH 27/30] fix: remove redundant change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index ce6798a6fa475..e77d14922e457 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit ce6798a6fa475c525fd04910efbb3f5618f2dcd3 +Subproject commit e77d14922e457ba4a284aea53d3e28b4bb26d567 From df7ebbae9baa59dd3718b16c18fa91ca9ccbe1ed Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 26 Nov 2025 14:53:16 +0700 Subject: [PATCH 28/30] fix: remove redundant change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index e77d14922e457..e30a1836e53c5 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit e77d14922e457ba4a284aea53d3e28b4bb26d567 +Subproject commit e30a1836e53c5fc5c568b211727827aa287cfeb0 From 779420e70de3e01cf4cc0f4c54a3f5968977fdf2 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 26 Nov 2025 15:12:58 +0700 Subject: [PATCH 29/30] fix: lint --- src/libs/actions/Search.ts | 12 ++++++------ src/pages/Search/SearchPage.tsx | 13 +++++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a69bccd42a64a..a21adc008ab4f 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -747,7 +747,7 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin rejectedCommentReportActionID: number; } > = {}; - transactionIDs.forEach((transactionID) => { + for (const transactionID of transactionIDs) { const data = prepareRejectMoneyRequestData(transactionID, reportID, comment, undefined, true); if (data) { optimisticData.push(...data.optimisticData); @@ -758,7 +758,7 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin rejectedCommentReportActionID: Number(data.parameters.rejectedCommentReportActionID), }; } - }); + } API.write( WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK, @@ -782,7 +782,7 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte return acc; }, {}); - Object.entries(transactionsByReport).forEach(([reportID, selectedTransactionIDs]) => { + for (const [reportID, selectedTransactionIDs] of Object.entries(transactionsByReport)) { const allReportTransactions = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const allTransactionIDs = allReportTransactions.map((transaction) => transaction.transactionID); const areAllExpensesSelected = allTransactionIDs.every((transactionID) => selectedTransactionIDs.includes(transactionID)); @@ -794,11 +794,11 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte } else { // Share a single destination ID across all rejections from the same source report const sharedRejectedToReportID = generateReportID(); - selectedTransactionIDs.forEach((transactionID) => { + for (const transactionID of selectedTransactionIDs) { rejectMoneyRequest(transactionID, reportID, comment, {sharedRejectedToReportID}); - }); + } } - }); + } } type Params = Record; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 6b8ad42a02768..d9430e855749a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -216,8 +216,17 @@ function SearchPage({route}: SearchPageProps) { } // Prefetch once per unique set of missing IDs - missingReportIDs.forEach((id) => id && openReport(id)); - missingPolicyIDs.forEach((id) => id && openWorkspace(id, [])); + for (const id of missingReportIDs) { + if (id) { + openReport(id); + } + } + + for (const id of missingPolicyIDs) { + if (id) { + openWorkspace(id, []); + } + } lastPrefetchKeyRef.current = key; }, [bulkRejectHydrationStatus, isOffline]); From 6f0ad368991402d06b584bb541743a0c09b853cb Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 26 Nov 2025 18:41:08 +0700 Subject: [PATCH 30/30] fix: update param's type --- src/libs/actions/Search.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a21adc008ab4f..7990a6ca06266 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -743,8 +743,8 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin const transactionIDToRejectReportAction: Record< string, { - rejectedActionReportActionID: number; - rejectedCommentReportActionID: number; + rejectedActionReportActionID: string; + rejectedCommentReportActionID: string; } > = {}; for (const transactionID of transactionIDs) { @@ -754,8 +754,8 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin successData.push(...data.successData); failureData.push(...data.failureData); transactionIDToRejectReportAction[transactionID] = { - rejectedActionReportActionID: Number(data.parameters.rejectedActionReportActionID), - rejectedCommentReportActionID: Number(data.parameters.rejectedCommentReportActionID), + rejectedActionReportActionID: data.parameters.rejectedActionReportActionID, + rejectedCommentReportActionID: data.parameters.rejectedCommentReportActionID, }; } }