Skip to content

Commit 4d31b8d

Browse files
authored
Merge pull request Expensify#94795 from thelullabyy/refactor-01/66378
Remove Onyx.connect() for the key: ONYXKEYS.COLLECTION.REPORT in src/libs/OptionsListUtils.ts (Part 1)
2 parents 6ea2890 + a01e0ef commit 4d31b8d

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
2+
3+
import ONYXKEYS from '@src/ONYXKEYS';
4+
import type {Report} from '@src/types/onyx';
5+
6+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
7+
8+
import useOnyx from './useOnyx';
9+
10+
type PolicyExpenseParticipant = {
11+
/** The report ID of the participant */
12+
reportID?: string;
13+
14+
/** Whether the participant is a policy expense chat */
15+
isPolicyExpenseChat?: boolean;
16+
};
17+
18+
/**
19+
* Resolves the expense reports for the policy expense chat participants without subscribing the consumer to the
20+
* entire reports collection, so callers only re-render when one of the relevant reports changes.
21+
*
22+
* Returns a `getReportByID` resolver (rather than the raw reports collection) because it matches the resolver
23+
* contract expected by `formatSectionsFromSearchTerm` and keeps the Onyx key construction encapsulated here
24+
* instead of being duplicated at every call site.
25+
*/
26+
export default function useSelectedExpenseReports(participants: readonly PolicyExpenseParticipant[]) {
27+
const expenseReportIDs = participants
28+
.filter((participant): participant is PolicyExpenseParticipant & {reportID: string} => !!participant.isPolicyExpenseChat && !!participant.reportID)
29+
.map((participant) => participant.reportID);
30+
31+
// React Compiler memoizes the selector on `expenseReportIDs`, so it re-runs whenever the set of relevant report IDs changes.
32+
const [expenseReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {
33+
selector: (reports: OnyxCollection<Report>) => {
34+
const result: OnyxCollection<Report> = {};
35+
for (const reportID of expenseReportIDs) {
36+
const key = `${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}` as const;
37+
result[key] = reports?.[key];
38+
}
39+
return result;
40+
},
41+
});
42+
43+
return (reportID: string | undefined): OnyxEntry<Report> => expenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`];
44+
}

src/libs/OptionsListUtils/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2977,6 +2977,9 @@ function formatSectionsFromSearchTerm(
29772977
shouldGetOptionDetails = false,
29782978
filteredWorkspaceChats: SearchOptionData[] = [],
29792979
reportAttributesDerived?: ReportAttributesDerivedValue['reports'],
2980+
// Resolves a single report by ID instead of receiving the whole reports collection, so callers only subscribe to the reports they actually need.
2981+
// The default falls back to the module-level Onyx.connect() cache until every caller passes a resolver (tracked in https://github.com/Expensify/App/issues/66378).
2982+
getReportByID: (reportID: string | undefined) => OnyxEntry<Report> = (reportID) => allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`],
29802983
): SectionForSearchTerm {
29812984
// We show the selected participants at the top of the list when there is no search term or maximum number of participants has already been selected
29822985
// However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results
@@ -2990,8 +2993,7 @@ function formatSectionsFromSearchTerm(
29902993
? selectedOptions.map((participant) => {
29912994
const isReportPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
29922995
if (isReportPolicyExpenseChat) {
2993-
// TODO: This allReports usage is temporary and will be removed once the full Onyx.connect() refactor is complete (https://github.com/Expensify/App/issues/66378)
2994-
const expenseReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`];
2996+
const expenseReport = getReportByID(participant.reportID);
29952997
const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${expenseReport?.reportID}`];
29962998
const expenseReportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${expenseReport?.policyID}`];
29972999
return getPolicyExpenseReportOption(participant, privateIsArchived, personalDetails, expenseReport, expenseReportPolicy, reportAttributesDerived);
@@ -3023,8 +3025,7 @@ function formatSectionsFromSearchTerm(
30233025
? selectedParticipantsWithoutDetails.map((participant) => {
30243026
const isReportPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
30253027
if (isReportPolicyExpenseChat) {
3026-
// TODO: This allReports usage is temporary and will be removed once the full Onyx.connect() refactor is complete (https://github.com/Expensify/App/issues/66378)
3027-
const expenseReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`];
3028+
const expenseReport = getReportByID(participant.reportID);
30283029
const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${expenseReport?.reportID}`];
30293030
const expenseReportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${expenseReport?.policyID}`];
30303031
return getPolicyExpenseReportOption(participant, privateIsArchived, personalDetails, expenseReport, expenseReportPolicy, reportAttributesDerived);

src/pages/iou/request/ParticipantSearchResults.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap';
1919
import useReportAttributes from '@hooks/useReportAttributes';
2020
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
2121
import useSearchSelector from '@hooks/useSearchSelector';
22+
import useSelectedExpenseReports from '@hooks/useSelectedExpenseReports';
2223
import useUserToInviteReports from '@hooks/useUserToInviteReports';
2324

2425
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
@@ -172,6 +173,7 @@ function ParticipantSearchResults({
172173
// Policy and billing data — owned here, used for getValidOptionsConfig and billing gate in onSelectRow
173174
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
174175
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
176+
const getReportByID = useSelectedExpenseReports(participants);
175177
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`];
176178
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
177179
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
@@ -304,6 +306,7 @@ function ParticipantSearchResults({
304306
true,
305307
undefined,
306308
reportAttributesDerived,
309+
getReportByID,
307310
);
308311
sections.push({...formatResults.section, sectionIndex: 0});
309312

tests/unit/OptionsListUtilsTest.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7905,6 +7905,62 @@ describe('OptionsListUtils', () => {
79057905

79067906
expect(result.section.data).toHaveLength(0);
79077907
});
7908+
7909+
it('should resolve the policy expense report through the getReportByID resolver instead of the module-level Onyx cache', () => {
7910+
// This report is intentionally NOT merged into Onyx, so it is only reachable through the resolver.
7911+
// If the function used the module-level Onyx.connect() cache, the report would not be found and
7912+
// private_isArchived would be undefined.
7913+
const resolverReportID = 'resolverOnlyReport1';
7914+
const resolverReport: Report = {
7915+
reportID: resolverReportID,
7916+
reportName: 'Resolver Workspace',
7917+
type: CONST.REPORT.TYPE.CHAT,
7918+
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
7919+
policyID: formatTestPolicyID,
7920+
ownerAccountID: formatOwnerAccountID,
7921+
participants: {
7922+
[formatOwnerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
7923+
},
7924+
};
7925+
const getReportByID = (reportID: string | undefined) => (reportID === resolverReportID ? resolverReport : undefined);
7926+
7927+
const privateIsArchivedMap: Record<string, boolean> = {
7928+
[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${resolverReportID}`]: true,
7929+
};
7930+
7931+
const selectedOptions: SearchOptionData[] = [
7932+
{
7933+
reportID: resolverReportID,
7934+
keyForList: resolverReportID,
7935+
isPolicyExpenseChat: true,
7936+
selected: true,
7937+
text: 'Resolver Workspace',
7938+
alternateText: '',
7939+
isSelected: true,
7940+
},
7941+
];
7942+
7943+
const result = formatSectionsFromSearchTerm(
7944+
'',
7945+
selectedOptions,
7946+
[],
7947+
[],
7948+
privateIsArchivedMap,
7949+
CURRENT_USER_ACCOUNT_ID,
7950+
undefined,
7951+
translateLocal,
7952+
formatPersonalDetails,
7953+
true,
7954+
undefined,
7955+
undefined,
7956+
getReportByID,
7957+
);
7958+
7959+
expect(result.section.data).toHaveLength(1);
7960+
7961+
const option = result.section.data.at(0);
7962+
expect(option?.private_isArchived).toBe(true);
7963+
});
79087964
});
79097965

79107966
describe('getUserToInviteOption', () => {

0 commit comments

Comments
 (0)