diff --git a/src/libs/actions/IOU/MoneyRequestBuilder.ts b/src/libs/actions/IOU/MoneyRequestBuilder.ts index 602fcb380140..9f8ec893bbbb 100644 --- a/src/libs/actions/IOU/MoneyRequestBuilder.ts +++ b/src/libs/actions/IOU/MoneyRequestBuilder.ts @@ -64,8 +64,9 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {Receipt, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getAllPersonalDetails, getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getCurrentUserPersonalDetails, getSearchOnyxUpdate, getUserAccountID} from './index'; +import {getAllPersonalDetails, getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getCurrentUserPersonalDetails, getUserAccountID} from './index'; import type {ReplaceReceipt, StartSplitBilActionParams} from './index'; +import {getSearchOnyxUpdate} from './SearchUpdate'; import type BasePolicyParams from './types/BasePolicyParams'; import type BaseTransactionParams from './types/BaseTransactionParams'; import type {CreateTrackExpenseParams} from './types/CreateTrackExpenseParams'; diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts new file mode 100644 index 000000000000..00cf4d8ff5a5 --- /dev/null +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -0,0 +1,239 @@ +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type {SearchQueryJSON} from '@components/Search/types'; +import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils'; +import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {getSuggestedSearches} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {OnyxData} from '@src/types/onyx/Request'; +import type {SearchResultDataType} from '@src/types/onyx/SearchResults'; +import {getCurrentUserPersonalDetails, getUserAccountID} from './index'; + +type ExpenseReportStatusPredicate = (expenseReport: OnyxEntry, transactionReportID?: string) => boolean; + +const expenseReportStatusFilterMapping: Record = { + [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN, + [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED, + [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, + [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport) => + (expenseReport?.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED, + [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, + [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, + [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, + [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, +}; + +type GetSearchOnyxUpdateParams = { + transaction: OnyxTypes.Transaction; + participant?: Participant; + iouReport?: OnyxEntry; + iouAction?: OnyxEntry; + policy?: OnyxEntry; + isFromOneTransactionReport?: boolean; + isInvoice?: boolean; + transactionThreadReportID: string | undefined; +}; + +// Determines whether the current search results should be optimistically updated +function shouldOptimisticallyUpdateSearch( + currentSearchQueryJSON: Readonly, + iouReport: OnyxEntry, + isInvoice: boolean | undefined, + transaction?: OnyxEntry, +) { + if ( + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.INVOICE && + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE && + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT + ) { + return false; + } + let shouldOptimisticallyUpdateByStatus; + const status = currentSearchQueryJSON.status; + const transactionReportID = transaction?.reportID; + if (Array.isArray(status)) { + shouldOptimisticallyUpdateByStatus = status.some((val) => { + const expenseStatus = val as ValueOf; + return expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); + }); + } else { + const expenseStatus = status as ValueOf; + shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); + } + + if (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) { + if (!currentSearchQueryJSON.policyID.includes(iouReport.policyID)) { + return false; + } + } + + if (!shouldOptimisticallyUpdateByStatus) { + return false; + } + + const suggestedSearches = getSuggestedSearches(getUserAccountID()); + const submitQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SUBMIT].searchQueryJSON; + const approveQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.APPROVE].searchQueryJSON; + const unapprovedCashSimilarSearchHash = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH].similarSearchHash; + + const validSearchTypes = + (!isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE) || + (iouReport?.type === CONST.REPORT.TYPE.EXPENSE && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT); + + const hasNoFlatFilters = currentSearchQueryJSON.flatFilters.length === 0; + + const matchesSubmitQuery = + submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport); + + const matchesApproveQuery = + approveQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport); + + const matchesUnapprovedCashQuery = + unapprovedCashSimilarSearchHash === currentSearchQueryJSON.similarSearchHash && + isExpenseReport(iouReport) && + (expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport) || expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport)) && + transaction?.reimbursable; + + const matchesFilterQuery = hasNoFlatFilters || matchesSubmitQuery || matchesApproveQuery || matchesUnapprovedCashQuery; + + return shouldOptimisticallyUpdateByStatus && validSearchTypes && matchesFilterQuery; +} + +function getSearchOnyxUpdate({ + participant, + transaction, + iouReport, + iouAction, + policy, + transactionThreadReportID, + isFromOneTransactionReport, + isInvoice, +}: GetSearchOnyxUpdateParams): OnyxData | undefined { + const toAccountID = participant?.accountID; + const deprecatedCurrentUserPersonalDetails = getCurrentUserPersonalDetails(); + const fromAccountID = deprecatedCurrentUserPersonalDetails?.accountID; + const currentSearchQueryJSON = getCurrentSearchQueryJSON(); + + if (!currentSearchQueryJSON || toAccountID === undefined || fromAccountID === undefined) { + return; + } + + if (shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, iouReport, isInvoice, transaction)) { + const isOptimisticToAccountData = isOptimisticPersonalDetail(toAccountID); + const successData = []; + if (isOptimisticToAccountData) { + // The optimistic personal detail is cleared from PERSONAL_DETAILS_LIST on API success, but the snapshot's report still references + // that optimistic accountID via report.managerID. Re-merging the personal detail into the snapshot in successData prevents the + // "To" column from briefly going blank before Search API delivers the real data. + // See https://github.com/Expensify/App/issues/61310 for more information. + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, + value: { + data: { + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [toAccountID]: { + accountID: toAccountID, + displayName: participant?.displayName, + login: participant?.login, + }, + }, + }, + }, + }); + } + // Building this object sequentially resolves TypeScript type inference issues + const optimisticSnapshotData: SearchResultDataType = {}; + + optimisticSnapshotData[ONYXKEYS.PERSONAL_DETAILS_LIST] = { + [toAccountID]: { + accountID: toAccountID, + displayName: participant?.displayName, + login: participant?.login, + }, + [fromAccountID]: { + accountID: fromAccountID, + avatar: deprecatedCurrentUserPersonalDetails?.avatar, + displayName: deprecatedCurrentUserPersonalDetails?.displayName, + login: deprecatedCurrentUserPersonalDetails?.login, + }, + }; + + optimisticSnapshotData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { + ...(transactionThreadReportID && {transactionThreadReportID}), + ...(isFromOneTransactionReport && {isFromOneTransactionReport}), + ...transaction, + }; + + if (policy) { + optimisticSnapshotData[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`] = policy; + } + + if (iouReport) { + optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`] = iouReport; + } + + if (iouReport && iouAction) { + optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`] = {[iouAction.reportActionID]: iouAction}; + } + + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, + value: { + data: optimisticSnapshotData, + }, + }, + ]; + + if (currentSearchQueryJSON.groupBy === CONST.SEARCH.GROUP_BY.FROM) { + const newFlatFilters = currentSearchQueryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM); + newFlatFilters.push({ + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, + filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: fromAccountID}], + }); + + const groupTransactionsQueryJSON = buildSearchQueryJSON( + buildSearchQueryString({ + ...currentSearchQueryJSON, + groupBy: undefined, + flatFilters: newFlatFilters, + }), + ); + + if (groupTransactionsQueryJSON?.hash) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${groupTransactionsQueryJSON.hash}` as const, + value: { + search: { + type: groupTransactionsQueryJSON.type, + status: groupTransactionsQueryJSON.status, + offset: 0, + hasMoreResults: false, + hasResults: true, + isLoading: false, + }, + data: optimisticSnapshotData, + }, + }); + } + } + + return { + optimisticData, + successData, + }; + } +} + +export {getSearchOnyxUpdate, shouldOptimisticallyUpdateSearch}; +export type {GetSearchOnyxUpdateParams}; diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 807b06de6c5f..63bd2d68bc16 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -35,9 +35,10 @@ import type {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report' import type {OnyxData} from '@src/types/onyx/Request'; import type {Receipt} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getAllPersonalDetails, getSearchOnyxUpdate} from '.'; +import {getAllPersonalDetails} from '.'; import {getReceiptError, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies} from './MoneyRequestBuilder'; import {handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded} from './NavigationHelpers'; +import {getSearchOnyxUpdate} from './SearchUpdate'; import type BasePolicyParams from './types/BasePolicyParams'; type SendInvoiceInformation = { diff --git a/src/libs/actions/IOU/TrackExpense.ts b/src/libs/actions/IOU/TrackExpense.ts index 8d52932f84bf..27fd9186defd 100644 --- a/src/libs/actions/IOU/TrackExpense.ts +++ b/src/libs/actions/IOU/TrackExpense.ts @@ -105,10 +105,11 @@ import type {Receipt, ReceiptSource} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {deleteMoneyRequest, getCleanUpTransactionThreadReportOnyxData, getNavigationUrlOnMoneyRequestDelete} from './DeleteMoneyRequest'; import type {ReplaceReceipt, StartSplitBilActionParams} from './index'; -import {getAllReports, getAllTransactionDrafts, getAllTransactions, getAllTransactionViolations, getMoneyRequestPolicyTags, getSearchOnyxUpdate} from './index'; +import {getAllReports, getAllTransactionDrafts, getAllTransactions, getAllTransactionViolations, getMoneyRequestPolicyTags} from './index'; import {buildMinimalTransactionForFormula, getMoneyRequestInformation, getReceiptError, getReportPreviewAction, getTransactionWithPreservedLocalReceiptSource} from './MoneyRequestBuilder'; import type {BuildOnyxDataForMoneyRequestKeys, RequestMoneyInformation} from './MoneyRequestBuilder'; import {handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded} from './NavigationHelpers'; +import {getSearchOnyxUpdate} from './SearchUpdate'; import type BasePolicyParams from './types/BasePolicyParams'; import type {CreateTrackExpenseParams} from './types/CreateTrackExpenseParams'; import type { diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 3168ade08c2d..a925e79fd4a2 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -1,8 +1,7 @@ import {format} from 'date-fns'; -import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxKey} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import type {SearchQueryJSON} from '@components/Search/types'; import type {UpdateMoneyRequestParams} from '@libs/API/parameters'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -14,15 +13,11 @@ import {getCustomUnitID} from '@libs/PerDiemRequestUtils'; import {getDistanceRateCustomUnit} from '@libs/PolicyUtils'; import { getReportOrDraftReport, - isExpenseReport, isInvoiceRoom, isMoneyRequestReport as isMoneyRequestReportReportUtils, - isOptimisticPersonalDetail, isPolicyExpenseChat as isPolicyExpenseChatReportUtil, isSelfDM, } from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; -import {getSuggestedSearches} from '@libs/SearchUIUtils'; import {startSpan} from '@libs/telemetry/activeSpans'; import {getCategoryTaxDetails, getDistanceInMeters, isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils} from '@libs/TransactionUtils'; import {getRemoveDraftTransactionsByIDsData, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; @@ -35,7 +30,6 @@ import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; import type {Unit} from '@src/types/onyx/Policy'; import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {SearchResultDataType} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type BaseTransactionParams from './types/BaseTransactionParams'; @@ -108,17 +102,6 @@ type ReplaceReceipt = { isSameReceipt?: boolean; }; -type GetSearchOnyxUpdateParams = { - transaction: OnyxTypes.Transaction; - participant?: Participant; - iouReport?: OnyxEntry; - iouAction?: OnyxEntry; - policy?: OnyxEntry; - isFromOneTransactionReport?: boolean; - isInvoice?: boolean; - transactionThreadReportID: string | undefined; -}; - let allTransactions: NonNullable> = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -846,216 +829,6 @@ function setMultipleMoneyRequestParticipantsFromReport(transactionIDs: string[], return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, updatedTransactions); } -type ExpenseReportStatusPredicate = (expenseReport: OnyxEntry, transactionReportID?: string) => boolean; - -const expenseReportStatusFilterMapping: Record = { - [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN, - [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport) => - expenseReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED, - [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, - [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport) => - (expenseReport?.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED, - [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, - [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, -}; - -// Determines whether the current search results should be optimistically updated -function shouldOptimisticallyUpdateSearch( - currentSearchQueryJSON: Readonly, - iouReport: OnyxEntry, - isInvoice: boolean | undefined, - transaction?: OnyxEntry, -) { - if ( - currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.INVOICE && - currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE && - currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT - ) { - return false; - } - let shouldOptimisticallyUpdateByStatus; - const status = currentSearchQueryJSON.status; - const transactionReportID = transaction?.reportID; - if (Array.isArray(status)) { - shouldOptimisticallyUpdateByStatus = status.some((val) => { - const expenseStatus = val as ValueOf; - return expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); - }); - } else { - const expenseStatus = status as ValueOf; - shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); - } - - if (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) { - if (!currentSearchQueryJSON.policyID.includes(iouReport.policyID)) { - return false; - } - } - - if (!shouldOptimisticallyUpdateByStatus) { - return false; - } - - const suggestedSearches = getSuggestedSearches(deprecatedUserAccountID); - const submitQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SUBMIT].searchQueryJSON; - const approveQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.APPROVE].searchQueryJSON; - const unapprovedCashSimilarSearchHash = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH].similarSearchHash; - - const validSearchTypes = - (!isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) || - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE) || - (iouReport?.type === CONST.REPORT.TYPE.EXPENSE && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT); - - const hasNoFlatFilters = currentSearchQueryJSON.flatFilters.length === 0; - - const matchesSubmitQuery = - submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport); - - const matchesApproveQuery = - approveQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport); - - const matchesUnapprovedCashQuery = - unapprovedCashSimilarSearchHash === currentSearchQueryJSON.similarSearchHash && - isExpenseReport(iouReport) && - (expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport) || expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport)) && - transaction?.reimbursable; - - const matchesFilterQuery = hasNoFlatFilters || matchesSubmitQuery || matchesApproveQuery || matchesUnapprovedCashQuery; - - return shouldOptimisticallyUpdateByStatus && validSearchTypes && matchesFilterQuery; -} - -function getSearchOnyxUpdate({ - participant, - transaction, - iouReport, - iouAction, - policy, - transactionThreadReportID, - isFromOneTransactionReport, - isInvoice, -}: GetSearchOnyxUpdateParams): OnyxData | undefined { - const toAccountID = participant?.accountID; - const fromAccountID = deprecatedCurrentUserPersonalDetails?.accountID; - const currentSearchQueryJSON = getCurrentSearchQueryJSON(); - - if (!currentSearchQueryJSON || toAccountID === undefined || fromAccountID === undefined) { - return; - } - - if (shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, iouReport, isInvoice, transaction)) { - const isOptimisticToAccountData = isOptimisticPersonalDetail(toAccountID); - const successData = []; - if (isOptimisticToAccountData) { - // The optimistic personal detail is cleared from PERSONAL_DETAILS_LIST on API success, but the snapshot's report still references - // that optimistic accountID via report.managerID. Re-merging the personal detail into the snapshot in successData prevents the - // "To" column from briefly going blank before Search API delivers the real data. - // See https://github.com/Expensify/App/issues/61310 for more information. - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, - value: { - data: { - [ONYXKEYS.PERSONAL_DETAILS_LIST]: { - [toAccountID]: { - accountID: toAccountID, - displayName: participant?.displayName, - login: participant?.login, - }, - }, - }, - }, - }); - } - // Building this object sequentially resolves TypeScript type inference issues - const optimisticSnapshotData: SearchResultDataType = {}; - - optimisticSnapshotData[ONYXKEYS.PERSONAL_DETAILS_LIST] = { - [toAccountID]: { - accountID: toAccountID, - displayName: participant?.displayName, - login: participant?.login, - }, - [fromAccountID]: { - accountID: fromAccountID, - avatar: deprecatedCurrentUserPersonalDetails?.avatar, - displayName: deprecatedCurrentUserPersonalDetails?.displayName, - login: deprecatedCurrentUserPersonalDetails?.login, - }, - }; - - optimisticSnapshotData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { - ...(transactionThreadReportID && {transactionThreadReportID}), - ...(isFromOneTransactionReport && {isFromOneTransactionReport}), - ...transaction, - }; - - if (policy) { - optimisticSnapshotData[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`] = policy; - } - - if (iouReport) { - optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`] = iouReport; - } - - if (iouReport && iouAction) { - optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`] = {[iouAction.reportActionID]: iouAction}; - } - - const optimisticData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, - value: { - data: optimisticSnapshotData, - }, - }, - ]; - - if (currentSearchQueryJSON.groupBy === CONST.SEARCH.GROUP_BY.FROM) { - const newFlatFilters = currentSearchQueryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM); - newFlatFilters.push({ - key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, - filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: fromAccountID}], - }); - - const groupTransactionsQueryJSON = buildSearchQueryJSON( - buildSearchQueryString({ - ...currentSearchQueryJSON, - groupBy: undefined, - flatFilters: newFlatFilters, - }), - ); - - if (groupTransactionsQueryJSON?.hash) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${groupTransactionsQueryJSON.hash}` as const, - value: { - search: { - type: groupTransactionsQueryJSON.type, - status: groupTransactionsQueryJSON.status, - offset: 0, - hasMoreResults: false, - hasResults: true, - isLoading: false, - }, - data: optimisticSnapshotData, - }, - }); - } - } - - return { - optimisticData, - successData, - }; - } -} - export { clearMoneyRequest, createDraftTransaction, @@ -1090,7 +863,6 @@ export { setMoneyRequestTaxRateValues, startMoneyRequest, updateLastLocationPermissionPrompt, - shouldOptimisticallyUpdateSearch, setMoneyRequestReimbursable, startDistanceRequest, getAllPersonalDetails, @@ -1106,7 +878,6 @@ export { getRecentAttendees, // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData (https://github.com/Expensify/App/issues/71491) with useOnyx hook getPolicyTagsData, - getSearchOnyxUpdate, getPolicyTags, getMoneyRequestPolicyTags, setMoneyRequestTimeRate, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 0ff646e63164..1a1b7a259c26 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -19,11 +19,11 @@ import { setMoneyRequestDistanceRate, setMoneyRequestMerchant, setMoneyRequestTag, - shouldOptimisticallyUpdateSearch, } from '@libs/actions/IOU'; import {putOnHold} from '@libs/actions/IOU/Hold'; import {calculateDiffAmount} from '@libs/actions/IOU/MoneyRequestBuilder'; import {handleNavigateAfterExpenseCreate} from '@libs/actions/IOU/NavigationHelpers'; +import {shouldOptimisticallyUpdateSearch} from '@libs/actions/IOU/SearchUpdate'; import {completeSplitBill, splitBill, startSplitBill} from '@libs/actions/IOU/Split'; import {updateSplitTransactionsFromSplitExpensesFlow} from '@libs/actions/IOU/SplitTransactionUpdate'; import {requestMoney, trackExpense} from '@libs/actions/IOU/TrackExpense';