From 4792b4231c034514cddad07dbc8a95a99d01719c Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 5 May 2026 17:04:53 +0700 Subject: [PATCH] refactor: extract SearchUpdate.ts from IOU/index.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts search-snapshot optimistic-update helpers (shouldOptimisticallyUpdateSearch, getSearchOnyxUpdate) along with their supporting type and predicate map into a dedicated SearchUpdate module. Function bodies are byte-identical to their pre-move state on main. The new module sets up its own Onyx subscriptions to SESSION and PERSONAL_DETAILS_LIST (mirroring index.ts) so it does not need to import from index.ts — this avoids the import/no-cycle violation that would arise because index.ts itself calls getSearchOnyxUpdate (inside buildOnyxDataForMoneyRequest). The two new deprecated-Onyx.connect violations are added to eslint-seatbelt.tsv. Part of Issue #72804 (break up src/libs/actions/IOU/). --- src/libs/actions/IOU/SearchUpdate.ts | 240 +++++++++++++++++++++++++++ src/libs/actions/IOU/SendInvoice.ts | 2 +- src/libs/actions/IOU/TrackExpense.ts | 2 +- src/libs/actions/IOU/index.ts | 236 +------------------------- tests/actions/IOUTest.ts | 2 +- 5 files changed, 251 insertions(+), 231 deletions(-) create mode 100644 src/libs/actions/IOU/SearchUpdate.ts diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts new file mode 100644 index 000000000000..6cc55d1ac3c4 --- /dev/null +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -0,0 +1,240 @@ +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'; +// eslint-disable-next-line import/no-cycle +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 9d256530a4df..d8e91e5bea03 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -38,12 +38,12 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import { getAllPersonalDetails, getReceiptError, - getSearchOnyxUpdate, handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies, } from '.'; +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 2d7c78e7fa4f..3ba07737c4ee 100644 --- a/src/libs/actions/IOU/TrackExpense.ts +++ b/src/libs/actions/IOU/TrackExpense.ts @@ -115,11 +115,11 @@ import { getMoneyRequestPolicyTags, getReceiptError, getReportPreviewAction, - getSearchOnyxUpdate, getTransactionWithPreservedLocalReceiptSource, handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded, } from './index'; +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 13acd104071b..62404af00524 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -3,7 +3,6 @@ import {fastMerge} from 'expensify-common'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxInputValue, OnyxKey, 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 type {UpdateMoneyRequestParams} from '@libs/API/parameters'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -47,7 +46,6 @@ import { isInvoiceRoom, isMoneyRequestReport as isMoneyRequestReportReportUtils, isOneTransactionReport, - isOptimisticPersonalDetail, isPolicyExpenseChat as isPolicyExpenseChatReportUtil, isSelectedManagerMcTest, isSelfDM, @@ -56,8 +54,6 @@ import { shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, updateReportPreview, } from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; -import {getSuggestedSearches} from '@libs/SearchUIUtils'; import {startSpan} from '@libs/telemetry/activeSpans'; import { buildOptimisticTransaction, @@ -89,9 +85,11 @@ import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; import type {ReportNextStep} from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {SearchDataTypes, SearchResultDataType} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +// eslint-disable-next-line import/no-cycle +import {getSearchOnyxUpdate} from './SearchUpdate'; import type BasePolicyParams from './types/BasePolicyParams'; import type BaseTransactionParams from './types/BaseTransactionParams'; import type {CreateTrackExpenseParams} from './types/CreateTrackExpenseParams'; @@ -323,17 +321,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, @@ -481,6 +468,10 @@ function getUserAccountID(): number { return deprecatedUserAccountID; } +function getCurrentUserPersonalDetails(): OnyxEntry { + return deprecatedCurrentUserPersonalDetails; +} + function getRecentAttendees(): OnyxEntry { return recentAttendees; } @@ -2509,216 +2500,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, @@ -2754,7 +2535,6 @@ export { setMoneyRequestTaxRateValues, startMoneyRequest, updateLastLocationPermissionPrompt, - shouldOptimisticallyUpdateSearch, setMoneyRequestReimbursable, calculateDiffAmount, getUpdatedMoneyRequestReportData, @@ -2771,12 +2551,12 @@ export { getAllTransactionDrafts, getCurrentUserEmail, getUserAccountID, + getCurrentUserPersonalDetails, getRecentAttendees, getReceiptError, // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData (https://github.com/Expensify/App/issues/71491) with useOnyx hook // eslint-disable-next-line @typescript-eslint/no-deprecated getPolicyTagsData, - getSearchOnyxUpdate, getPolicyTags, // eslint-disable-next-line @typescript-eslint/no-deprecated getMoneyRequestPolicyTags, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 64d052648c2c..897d605679b4 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -21,9 +21,9 @@ import { setMoneyRequestDistanceRate, setMoneyRequestMerchant, setMoneyRequestTag, - shouldOptimisticallyUpdateSearch, } from '@libs/actions/IOU'; import {putOnHold} from '@libs/actions/IOU/Hold'; +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';