diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 84b580b9b103..835d89cf1cfc 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -892,7 +892,8 @@ function useSearchBulkActions({queryJSON, deleteTransactionsOnSearch}: UseSearch const selectedTransactionsList = Object.values(selectedTransactions) .map((transaction) => transaction.transaction) .filter((transaction): transaction is Transaction => !!transaction); - const canEditMultiple = canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, policies, isExpenseReportSearch) && isBetaEnabled(CONST.BETAS.BULK_EDIT); + const canEditMultiple = + canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, policies, isExpenseReportSearch, searchResults?.data) && isBetaEnabled(CONST.BETAS.BULK_EDIT); if (canEditMultiple) { options.push({ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f02df86b287a..dffd85af8af6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -56,6 +56,7 @@ import type { ReportMetadata, ReportNameValuePairs, ReportViolationName, + SearchResults, Task, Transaction, TransactionViolation, @@ -4876,6 +4877,7 @@ function canEditMultipleTransactions( reports: OnyxCollection, policies: OnyxCollection, areReportsSelected = false, + searchSnapshotData?: SearchResults['data'], ): boolean { if (areReportsSelected) { return false; @@ -4896,7 +4898,9 @@ function canEditMultipleTransactions( return false; } - const reportAction = getIOUActionForTransactionID(Object.values(reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {}), transaction.transactionID); + const reportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}` as const; + const actionsForReport = {...(searchSnapshotData?.[reportActionsKey] ?? {}), ...(reportActions?.[reportActionsKey] ?? {})}; + const reportAction = getIOUActionForTransactionID(Object.values(actionsForReport), transaction.transactionID); const report = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index df71b0ac05d4..cac955656ee1 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -377,12 +377,12 @@ const ViolationsUtils = { } // Remove 'missingCategory' violation if category is valid according to policy - if (hasMissingCategoryViolation && (isCategoryInPolicy || isSelfDM)) { + if (hasMissingCategoryViolation && (isCategoryInPolicy || isSelfDM || isInvoiceTransaction)) { newTransactionViolations = reject(newTransactionViolations, {name: 'missingCategory'}); } // Add 'missingCategory' violation if category is required and not set - if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey && !isSelfDM) { + if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey && !isSelfDM && !isInvoiceTransaction) { newTransactionViolations.push({name: 'missingCategory', type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}); } } diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx index 243fd6b6203e..1e841610fabc 100644 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx +++ b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx @@ -1,5 +1,6 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -24,13 +25,50 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; +import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx'; import type {TransactionChanges} from '@src/types/onyx/Transaction'; import {getTransactionEditContext} from './SearchEditMultipleUtils'; +/** + * After a hard refresh, invoice transaction and report action data may only exist in the search snapshot, + * not in the main Onyx collections. These helpers fill gaps from the snapshot so bulk edit can work. + */ +function withSnapshotTransactions(onyxTransactions: OnyxCollection | undefined, snapshotData: SearchResults['data'] | undefined): OnyxCollection | undefined { + if (!snapshotData) { + return onyxTransactions; + } + const merged = {...onyxTransactions}; + for (const key of Object.keys(snapshotData)) { + if (!key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { + continue; + } + const typedKey = key as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`; + if (!merged[typedKey]) { + merged[typedKey] = snapshotData[typedKey] ?? null; + } + } + return merged; +} + +function withSnapshotReportActions(onyxReportActions: OnyxCollection | undefined, snapshotData: SearchResults['data'] | undefined): OnyxCollection | undefined { + if (!snapshotData) { + return onyxReportActions; + } + const merged = {...onyxReportActions}; + for (const key of Object.keys(snapshotData)) { + if (!key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)) { + continue; + } + const typedKey = key as `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`; + merged[typedKey] = {...(snapshotData[typedKey] ?? {}), ...(merged[typedKey] ?? {})}; + } + return merged; +} + function SearchEditMultiplePage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {currentSearchHash} = useSearchStateContext(); + const {currentSearchHash, currentSearchResults} = useSearchStateContext(); const {clearSelectedTransactions} = useSearchActionsContext(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); @@ -39,10 +77,14 @@ function SearchEditMultiplePage() { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const snapshotData = currentSearchResults?.data; + const mergedTransactions = withSnapshotTransactions(allTransactions, snapshotData); + const mergedReportActions = withSnapshotReportActions(allReportActions, snapshotData); + const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? []; const selectedTransactionContexts = selectedTransactionIDs.flatMap((transactionID) => { - const context = getTransactionEditContext(transactionID, allTransactions, allReports, allReportActions, policies); + const context = getTransactionEditContext(transactionID, mergedTransactions, allReports, mergedReportActions, policies); return context ? [context] : []; }); @@ -85,7 +127,7 @@ function SearchEditMultiplePage() { return !isIOUReport(report) && !isInvoiceReport(report) && transactionPolicy?.disabledFields?.reimbursable === false && !isManagedCardTransaction(transaction); }); - const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, allTransactions, allReports); + const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, mergedTransactions, allReports); const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined; const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); @@ -152,8 +194,8 @@ function SearchEditMultiplePage() { changes, policy, reports: allReports, - transactions: allTransactions, - reportActions: allReportActions, + transactions: mergedTransactions, + reportActions: mergedReportActions, policyCategories, hash: currentSearchHash, allPolicies: policies,