Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
isIndividualInvoiceRoom,
isInvoiceReport,
isIOUReport as isIOUReportUtil,
isOneTransactionReport,
isSelfDM,
shouldShowMarkAsDone,
} from '@libs/ReportUtils';
Expand Down Expand Up @@ -1759,7 +1760,17 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const areFullReportsSelected = selectedTransactionReportIDs.length === selectedReportIDs.length && selectedTransactionReportIDs.every((id) => selectedReportIDs.includes(id));
const typeInvoice = queryJSON?.type === CONST.REPORT.TYPE.INVOICE;
const typeExpense = queryJSON?.type === CONST.REPORT.TYPE.EXPENSE;
const isAllOneTransactionReport = Object.values(selectedTransactions).every((transaction) => transaction.isFromOneTransactionReport);
// Derive the one-transaction-report check from the live search snapshot rather than the value frozen
// into each selection at click time. When a row is selected before its report finishes hydrating,
// the frozen `isFromOneTransactionReport` can be stale (e.g. `false` because `transactionCount` wasn't
// loaded yet), which produced the wrong export options until the user unselected and reselected.
// Resolving the report live lets the options recompute as soon as report data lands (both
// `currentSearchResults?.data` and `allReports` are already memo deps), with a fallback to the frozen
// value when live data isn't available.
const isAllOneTransactionReport = Object.values(selectedTransactions).every((transaction) => {
const snapshotReport = transaction.reportID ? getReportFromSearchSnapshot(transaction.reportID, currentSearchResults?.data, allReports) : undefined;
return snapshotReport ? isOneTransactionReport(snapshotReport) : transaction.isFromOneTransactionReport;
});

const includeReportLevelExport = ((isExpenseReportType || typeInvoice) && areFullReportsSelected) || (typeExpense && !isExpenseReportType && isAllOneTransactionReport);

Expand Down
4 changes: 4 additions & 0 deletions tests/unit/hooks/useSearchBulkActionsDeleteTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jest.mock('@libs/actions/Search', () => ({
getLastPolicyPaymentMethod: jest.fn(),
getPayMoneyOnSearchInvoiceParams: jest.fn(),
getPayOption: jest.fn(() => ({shouldEnableBulkPayOption: false, isFirstTimePayment: false})),
// Faithful mock of the real helper: prefer the search snapshot, fall back to live Onyx.
getReportFromSearchSnapshot: jest.fn((reportID?: string, searchData?: Record<string, unknown>, allReports?: Record<string, unknown>) =>
reportID ? (searchData?.[`report_${reportID}`] ?? allReports?.[`report_${reportID}`]) : undefined,
),
getReportType: jest.fn(),
getTotalFormattedAmount: jest.fn(() => ''),
isCurrencySupportWalletBulkPay: jest.fn(() => false),
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/hooks/useSearchBulkActionsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jest.mock('@libs/actions/Search', () => ({
getLastPolicyPaymentMethod: jest.fn(),
getPayMoneyOnSearchInvoiceParams: jest.fn(),
getPayOption: jest.fn(() => ({shouldEnableBulkPayOption: false, isFirstTimePayment: false})),
// Faithful mock of the real helper: prefer the search snapshot, fall back to live Onyx.
getReportFromSearchSnapshot: jest.fn((reportID?: string, searchData?: Record<string, unknown>, allReports?: Record<string, unknown>) =>
reportID ? (searchData?.[`report_${reportID}`] ?? allReports?.[`report_${reportID}`]) : undefined,
),
getReportType: jest.fn(),
getTotalFormattedAmount: jest.fn(() => ''),
isCurrencySupportWalletBulkPay: jest.fn(() => false),
Expand Down
Loading