Skip to content

Commit a175054

Browse files
authored
Merge pull request Expensify#89590 from DylanDylann/refactor-search-update
[No QA]: refactor: extract SearchUpdate.ts from IOU/index.ts
2 parents 6832ae4 + 96caa17 commit a175054

6 files changed

Lines changed: 247 additions & 234 deletions

File tree

src/libs/actions/IOU/MoneyRequestBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ import type ReportAction from '@src/types/onyx/ReportAction';
6464
import type {OnyxData} from '@src/types/onyx/Request';
6565
import type {Receipt, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction';
6666
import {isEmptyObject} from '@src/types/utils/EmptyObject';
67-
import {getAllPersonalDetails, getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getCurrentUserPersonalDetails, getSearchOnyxUpdate, getUserAccountID} from './index';
67+
import {getAllPersonalDetails, getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getCurrentUserPersonalDetails, getUserAccountID} from './index';
6868
import type {ReplaceReceipt, StartSplitBilActionParams} from './index';
69+
import {getSearchOnyxUpdate} from './SearchUpdate';
6970
import type BasePolicyParams from './types/BasePolicyParams';
7071
import type BaseTransactionParams from './types/BaseTransactionParams';
7172
import type {CreateTrackExpenseParams} from './types/CreateTrackExpenseParams';
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
2+
import Onyx from 'react-native-onyx';
3+
import type {ValueOf} from 'type-fest';
4+
import type {SearchQueryJSON} from '@components/Search/types';
5+
import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils';
6+
import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
7+
import {getSuggestedSearches} from '@libs/SearchUIUtils';
8+
import CONST from '@src/CONST';
9+
import ONYXKEYS from '@src/ONYXKEYS';
10+
import type * as OnyxTypes from '@src/types/onyx';
11+
import type {Participant} from '@src/types/onyx/IOU';
12+
import type {OnyxData} from '@src/types/onyx/Request';
13+
import type {SearchResultDataType} from '@src/types/onyx/SearchResults';
14+
import {getCurrentUserPersonalDetails, getUserAccountID} from './index';
15+
16+
type ExpenseReportStatusPredicate = (expenseReport: OnyxEntry<OnyxTypes.Report>, transactionReportID?: string) => boolean;
17+
18+
const expenseReportStatusFilterMapping: Record<string, ExpenseReportStatusPredicate> = {
19+
[CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN,
20+
[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport) =>
21+
expenseReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED,
22+
[CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED,
23+
[CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport) =>
24+
(expenseReport?.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED,
25+
[CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED,
26+
[CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID,
27+
[CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID,
28+
[CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true,
29+
};
30+
31+
type GetSearchOnyxUpdateParams = {
32+
transaction: OnyxTypes.Transaction;
33+
participant?: Participant;
34+
iouReport?: OnyxEntry<OnyxTypes.Report>;
35+
iouAction?: OnyxEntry<OnyxTypes.ReportAction>;
36+
policy?: OnyxEntry<OnyxTypes.Policy>;
37+
isFromOneTransactionReport?: boolean;
38+
isInvoice?: boolean;
39+
transactionThreadReportID: string | undefined;
40+
};
41+
42+
// Determines whether the current search results should be optimistically updated
43+
function shouldOptimisticallyUpdateSearch(
44+
currentSearchQueryJSON: Readonly<SearchQueryJSON>,
45+
iouReport: OnyxEntry<OnyxTypes.Report>,
46+
isInvoice: boolean | undefined,
47+
transaction?: OnyxEntry<OnyxTypes.Transaction>,
48+
) {
49+
if (
50+
currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.INVOICE &&
51+
currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE &&
52+
currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT
53+
) {
54+
return false;
55+
}
56+
let shouldOptimisticallyUpdateByStatus;
57+
const status = currentSearchQueryJSON.status;
58+
const transactionReportID = transaction?.reportID;
59+
if (Array.isArray(status)) {
60+
shouldOptimisticallyUpdateByStatus = status.some((val) => {
61+
const expenseStatus = val as ValueOf<typeof CONST.SEARCH.STATUS.EXPENSE>;
62+
return expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID);
63+
});
64+
} else {
65+
const expenseStatus = status as ValueOf<typeof CONST.SEARCH.STATUS.EXPENSE>;
66+
shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID);
67+
}
68+
69+
if (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) {
70+
if (!currentSearchQueryJSON.policyID.includes(iouReport.policyID)) {
71+
return false;
72+
}
73+
}
74+
75+
if (!shouldOptimisticallyUpdateByStatus) {
76+
return false;
77+
}
78+
79+
const suggestedSearches = getSuggestedSearches(getUserAccountID());
80+
const submitQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SUBMIT].searchQueryJSON;
81+
const approveQueryJSON = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.APPROVE].searchQueryJSON;
82+
const unapprovedCashSimilarSearchHash = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH].similarSearchHash;
83+
84+
const validSearchTypes =
85+
(!isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) ||
86+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
87+
(isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE) ||
88+
(iouReport?.type === CONST.REPORT.TYPE.EXPENSE && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT);
89+
90+
const hasNoFlatFilters = currentSearchQueryJSON.flatFilters.length === 0;
91+
92+
const matchesSubmitQuery =
93+
submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport);
94+
95+
const matchesApproveQuery =
96+
approveQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport);
97+
98+
const matchesUnapprovedCashQuery =
99+
unapprovedCashSimilarSearchHash === currentSearchQueryJSON.similarSearchHash &&
100+
isExpenseReport(iouReport) &&
101+
(expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport) || expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport)) &&
102+
transaction?.reimbursable;
103+
104+
const matchesFilterQuery = hasNoFlatFilters || matchesSubmitQuery || matchesApproveQuery || matchesUnapprovedCashQuery;
105+
106+
return shouldOptimisticallyUpdateByStatus && validSearchTypes && matchesFilterQuery;
107+
}
108+
109+
function getSearchOnyxUpdate({
110+
participant,
111+
transaction,
112+
iouReport,
113+
iouAction,
114+
policy,
115+
transactionThreadReportID,
116+
isFromOneTransactionReport,
117+
isInvoice,
118+
}: GetSearchOnyxUpdateParams): OnyxData<typeof ONYXKEYS.COLLECTION.SNAPSHOT> | undefined {
119+
const toAccountID = participant?.accountID;
120+
const deprecatedCurrentUserPersonalDetails = getCurrentUserPersonalDetails();
121+
const fromAccountID = deprecatedCurrentUserPersonalDetails?.accountID;
122+
const currentSearchQueryJSON = getCurrentSearchQueryJSON();
123+
124+
if (!currentSearchQueryJSON || toAccountID === undefined || fromAccountID === undefined) {
125+
return;
126+
}
127+
128+
if (shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, iouReport, isInvoice, transaction)) {
129+
const isOptimisticToAccountData = isOptimisticPersonalDetail(toAccountID);
130+
const successData = [];
131+
if (isOptimisticToAccountData) {
132+
// The optimistic personal detail is cleared from PERSONAL_DETAILS_LIST on API success, but the snapshot's report still references
133+
// that optimistic accountID via report.managerID. Re-merging the personal detail into the snapshot in successData prevents the
134+
// "To" column from briefly going blank before Search API delivers the real data.
135+
// See https://github.com/Expensify/App/issues/61310 for more information.
136+
successData.push({
137+
onyxMethod: Onyx.METHOD.MERGE,
138+
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const,
139+
value: {
140+
data: {
141+
[ONYXKEYS.PERSONAL_DETAILS_LIST]: {
142+
[toAccountID]: {
143+
accountID: toAccountID,
144+
displayName: participant?.displayName,
145+
login: participant?.login,
146+
},
147+
},
148+
},
149+
},
150+
});
151+
}
152+
// Building this object sequentially resolves TypeScript type inference issues
153+
const optimisticSnapshotData: SearchResultDataType = {};
154+
155+
optimisticSnapshotData[ONYXKEYS.PERSONAL_DETAILS_LIST] = {
156+
[toAccountID]: {
157+
accountID: toAccountID,
158+
displayName: participant?.displayName,
159+
login: participant?.login,
160+
},
161+
[fromAccountID]: {
162+
accountID: fromAccountID,
163+
avatar: deprecatedCurrentUserPersonalDetails?.avatar,
164+
displayName: deprecatedCurrentUserPersonalDetails?.displayName,
165+
login: deprecatedCurrentUserPersonalDetails?.login,
166+
},
167+
};
168+
169+
optimisticSnapshotData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = {
170+
...(transactionThreadReportID && {transactionThreadReportID}),
171+
...(isFromOneTransactionReport && {isFromOneTransactionReport}),
172+
...transaction,
173+
};
174+
175+
if (policy) {
176+
optimisticSnapshotData[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`] = policy;
177+
}
178+
179+
if (iouReport) {
180+
optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`] = iouReport;
181+
}
182+
183+
if (iouReport && iouAction) {
184+
optimisticSnapshotData[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`] = {[iouAction.reportActionID]: iouAction};
185+
}
186+
187+
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [
188+
{
189+
onyxMethod: Onyx.METHOD.MERGE,
190+
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const,
191+
value: {
192+
data: optimisticSnapshotData,
193+
},
194+
},
195+
];
196+
197+
if (currentSearchQueryJSON.groupBy === CONST.SEARCH.GROUP_BY.FROM) {
198+
const newFlatFilters = currentSearchQueryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM);
199+
newFlatFilters.push({
200+
key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM,
201+
filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: fromAccountID}],
202+
});
203+
204+
const groupTransactionsQueryJSON = buildSearchQueryJSON(
205+
buildSearchQueryString({
206+
...currentSearchQueryJSON,
207+
groupBy: undefined,
208+
flatFilters: newFlatFilters,
209+
}),
210+
);
211+
212+
if (groupTransactionsQueryJSON?.hash) {
213+
optimisticData.push({
214+
onyxMethod: Onyx.METHOD.MERGE,
215+
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${groupTransactionsQueryJSON.hash}` as const,
216+
value: {
217+
search: {
218+
type: groupTransactionsQueryJSON.type,
219+
status: groupTransactionsQueryJSON.status,
220+
offset: 0,
221+
hasMoreResults: false,
222+
hasResults: true,
223+
isLoading: false,
224+
},
225+
data: optimisticSnapshotData,
226+
},
227+
});
228+
}
229+
}
230+
231+
return {
232+
optimisticData,
233+
successData,
234+
};
235+
}
236+
}
237+
238+
export {getSearchOnyxUpdate, shouldOptimisticallyUpdateSearch};
239+
export type {GetSearchOnyxUpdateParams};

src/libs/actions/IOU/SendInvoice.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ import type {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report'
3535
import type {OnyxData} from '@src/types/onyx/Request';
3636
import type {Receipt} from '@src/types/onyx/Transaction';
3737
import {isEmptyObject} from '@src/types/utils/EmptyObject';
38-
import {getAllPersonalDetails, getSearchOnyxUpdate} from '.';
38+
import {getAllPersonalDetails} from '.';
3939
import {getReceiptError, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies} from './MoneyRequestBuilder';
4040
import {handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded} from './NavigationHelpers';
41+
import {getSearchOnyxUpdate} from './SearchUpdate';
4142
import type BasePolicyParams from './types/BasePolicyParams';
4243

4344
type SendInvoiceInformation = {

src/libs/actions/IOU/TrackExpense.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ import type {Receipt, ReceiptSource} from '@src/types/onyx/Transaction';
105105
import {isEmptyObject} from '@src/types/utils/EmptyObject';
106106
import {deleteMoneyRequest, getCleanUpTransactionThreadReportOnyxData, getNavigationUrlOnMoneyRequestDelete} from './DeleteMoneyRequest';
107107
import type {ReplaceReceipt, StartSplitBilActionParams} from './index';
108-
import {getAllReports, getAllTransactionDrafts, getAllTransactions, getAllTransactionViolations, getMoneyRequestPolicyTags, getSearchOnyxUpdate} from './index';
108+
import {getAllReports, getAllTransactionDrafts, getAllTransactions, getAllTransactionViolations, getMoneyRequestPolicyTags} from './index';
109109
import {buildMinimalTransactionForFormula, getMoneyRequestInformation, getReceiptError, getReportPreviewAction, getTransactionWithPreservedLocalReceiptSource} from './MoneyRequestBuilder';
110110
import type {BuildOnyxDataForMoneyRequestKeys, RequestMoneyInformation} from './MoneyRequestBuilder';
111111
import {handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded} from './NavigationHelpers';
112+
import {getSearchOnyxUpdate} from './SearchUpdate';
112113
import type BasePolicyParams from './types/BasePolicyParams';
113114
import type {CreateTrackExpenseParams} from './types/CreateTrackExpenseParams';
114115
import type {

0 commit comments

Comments
 (0)