Skip to content

Commit 3f88b9c

Browse files
committed
Merge branch 'main' into feat/migrate-category-selector-to-navigation
2 parents c64a733 + fba471a commit 3f88b9c

20 files changed

Lines changed: 270 additions & 59 deletions

src/ROUTES.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,11 @@ const ROUTES = {
12361236
return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const;
12371237
},
12381238
},
1239+
MONEY_REQUEST_CREATE_VERIFY_ACCOUNT: {
1240+
route: `:action/:iouType/start/:transactionID/:reportID/${VERIFY_ACCOUNT}`,
1241+
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) =>
1242+
`${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${VERIFY_ACCOUNT}` as const,
1243+
},
12391244
MONEY_REQUEST_STEP_SEND_FROM: {
12401245
route: 'create/:iouType/from/:transactionID/:reportID',
12411246

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ const SCREENS = {
344344

345345
MONEY_REQUEST: {
346346
CREATE: 'Money_Request_Create',
347+
CREATE_VERIFY_ACCOUNT: 'Money_Request_Create_Verify_Account',
347348
HOLD: 'Money_Request_Hold_Reason',
348349
REJECT: 'Money_Request_Reject_Reason',
349350
REPORT_REJECT: 'Money_Request_Report_Reject',

src/components/MigratedUserWelcomeModal.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import {openExternalLink} from '@libs/actions/Link';
1111
import {dismissProductTraining} from '@libs/actions/Welcome';
1212
import convertToLTR from '@libs/convertToLTR';
1313
import Log from '@libs/Log';
14+
import Navigation from '@libs/Navigation/Navigation';
15+
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
1416
import variables from '@styles/variables';
1517
import CONST from '@src/CONST';
18+
import ROUTES from '@src/ROUTES';
1619
import type {FeatureListItem} from './FeatureList';
1720
import FeatureTrainingModal from './FeatureTrainingModal';
1821
import Icon from './Icon';
@@ -58,6 +61,7 @@ function MigratedUserWelcomeModal() {
5861
const onClose = () => {
5962
Log.hmmm('[MigratedUserWelcomeModal] onClose called, dismissing product training');
6063
dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL);
64+
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})}));
6165
};
6266

6367
const featureListContent = (

src/components/ReferralProgramCTA.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function ReferralProgramCTA({referralContentType, style, onDismiss}: ReferralPro
6464
{/* Hidden from accessibility — the CTA pressable above already announces this content. */}
6565
<View
6666
aria-hidden
67+
pointerEvents="none"
6768
style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}
6869
>
6970
<RenderHTML html={translate(`referralProgram.${referralContentType}.buttonText`)} />

src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const SKIPPED_FILTERS = new Set<SearchAdvancedFiltersKey>([
6060
FILTER_KEYS.VIEW,
6161
FILTER_KEYS.PAYER,
6262
FILTER_KEYS.ACTION,
63+
FILTER_KEYS.COLUMNS,
6364
]);
6465

6566
function getFilterSentryLabel(filterKey: SearchAdvancedFiltersKey | SearchFilterKey | ReportFieldKey) {
Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,50 @@
1+
import type {OnyxCollection} from 'react-native-onyx';
12
import {isPaidGroupPolicy, isPolicyAccessible} from '@libs/PolicyUtils';
23
import ONYXKEYS from '@src/ONYXKEYS';
4+
import type {Policy} from '@src/types/onyx';
35
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
46
import useOnyx from './useOnyx';
57
import usePreferredPolicy from './usePreferredPolicy';
68

9+
/**
10+
* Selector that finds the single qualifying group policy ID from the collection.
11+
* Returns only an ID (stable) — prevents re-renders when unrelated policies change.
12+
*/
13+
function getSingleGroupPolicyID(policies: OnyxCollection<Policy>, login: string): string | undefined {
14+
if (!policies) {
15+
return undefined;
16+
}
17+
18+
let singlePolicyID: string | undefined;
19+
for (const policy of Object.values(policies)) {
20+
if (!policy || !isPaidGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
21+
continue;
22+
}
23+
if (!singlePolicyID) {
24+
singlePolicyID = policy.id;
25+
} else {
26+
return undefined; // More than one — no single default
27+
}
28+
}
29+
30+
return singlePolicyID;
31+
}
32+
733
export default function useDefaultExpensePolicy() {
834
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
935
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
10-
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
1136
const {isRestrictedToPreferredPolicy, preferredPolicyID} = usePreferredPolicy();
1237
const {login = ''} = useCurrentUserPersonalDetails();
1338
const [preferredPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${preferredPolicyID}`);
1439

40+
// Selector returns only the qualifying policy ID — stable value, prevents re-renders
41+
const [singleGroupPolicyID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
42+
selector: (policies) => getSingleGroupPolicyID(policies, login),
43+
});
44+
45+
// Per-key lookup for the single group policy (only fires when that specific policy changes)
46+
const [singleGroupPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${singleGroupPolicyID}`);
47+
1548
if (isRestrictedToPreferredPolicy && isPaidGroupPolicy(preferredPolicy) && isPolicyAccessible(preferredPolicy, login)) {
1649
return preferredPolicy;
1750
}
@@ -20,20 +53,5 @@ export default function useDefaultExpensePolicy() {
2053
return activePolicy;
2154
}
2255

23-
// If there is exactly one group policy, use that as the default expense policy
24-
let singlePolicy;
25-
for (const policy of Object.values(allPolicies ?? {})) {
26-
if (!policy || !isPaidGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
27-
continue;
28-
}
29-
30-
if (!singlePolicy) {
31-
singlePolicy = policy;
32-
} else {
33-
singlePolicy = undefined;
34-
break;
35-
}
36-
}
37-
38-
return singlePolicy;
56+
return singleGroupPolicy;
3957
}

src/hooks/usePolicyForMovingExpenses.ts

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {activePolicySelector} from '@selectors/Policy';
2-
import type {OnyxEntry} from 'react-native-onyx';
2+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
33
import {useSession} from '@components/OnyxListItemProvider';
44
import {canSubmitPerDiemExpenseFromWorkspace, isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete, isTimeTrackingEnabled} from '@libs/PolicyUtils';
55
import CONST from '@src/CONST';
@@ -31,48 +31,85 @@ function isPolicyValidForMovingExpenses(policy: OnyxEntry<Policy>, login: string
3131
);
3232
}
3333

34-
function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) {
35-
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
36-
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
37-
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {
38-
selector: activePolicySelector,
39-
});
34+
type PolicyQualificationResult = {
35+
singlePolicyID: string | undefined;
36+
isMemberOfMoreThanOnePolicy: boolean;
37+
validExpensePolicyID: string | undefined;
38+
};
4039

41-
const session = useSession();
42-
const login = session?.email ?? '';
40+
/**
41+
* Selector that computes which policies qualify for moving expenses.
42+
* Returns only IDs and flags — stable output that prevents re-renders when unrelated policies change.
43+
*/
44+
function getPolicyQualificationResult(
45+
policies: OnyxCollection<Policy>,
46+
login: string,
47+
isPerDiemRequest?: boolean,
48+
isTimeRequest?: boolean,
49+
expensePolicyID?: string,
50+
): PolicyQualificationResult {
51+
if (!policies) {
52+
return {singlePolicyID: undefined, isMemberOfMoreThanOnePolicy: false, validExpensePolicyID: undefined};
53+
}
4354

44-
// Early exit optimization: only need to check if we have 0, 1, or >1 policies
45-
let singleUserPolicy;
55+
let singlePolicyID: string | undefined;
4656
let isMemberOfMoreThanOnePolicy = false;
47-
for (const policy of Object.values(allPolicies ?? {})) {
57+
for (const policy of Object.values(policies)) {
4858
if (!isPolicyValidForMovingExpenses(policy, login, isPerDiemRequest, isTimeRequest)) {
4959
continue;
5060
}
51-
52-
if (!singleUserPolicy) {
53-
singleUserPolicy = policy;
61+
if (!singlePolicyID) {
62+
singlePolicyID = policy?.id;
5463
} else {
5564
isMemberOfMoreThanOnePolicy = true;
56-
break; // Found 2, no need to continue
65+
break;
5766
}
5867
}
5968

60-
// If an expense policy ID is provided and valid, prefer it over the active policy
61-
// This ensures that when viewing/editing an expense from workspace B, we show workspace B
62-
// even if the user's default workspace is A
69+
let validExpensePolicyID: string | undefined;
6370
if (expensePolicyID) {
64-
const expensePolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${expensePolicyID}`];
71+
const expensePolicy = policies[`${ONYXKEYS.COLLECTION.POLICY}${expensePolicyID}`];
6572
if (expensePolicy && isPolicyValidForMovingExpenses(expensePolicy, login, isPerDiemRequest, isTimeRequest)) {
66-
return {policyForMovingExpensesID: expensePolicyID, policyForMovingExpenses: expensePolicy, shouldSelectPolicy: false};
73+
validExpensePolicyID = expensePolicyID;
6774
}
6875
}
6976

77+
return {singlePolicyID, isMemberOfMoreThanOnePolicy, validExpensePolicyID};
78+
}
79+
80+
function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) {
81+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
82+
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {
83+
selector: activePolicySelector,
84+
});
85+
86+
const session = useSession();
87+
const login = session?.email ?? '';
88+
89+
// Contextual selector — captures login/flags from closure.
90+
// Returns only IDs + flags (stable output) to prevent re-renders when unrelated policies change.
91+
const policyQualificationSelector = (policies: OnyxCollection<Policy>) => getPolicyQualificationResult(policies, login, isPerDiemRequest, isTimeRequest, expensePolicyID);
92+
const [qualificationResult] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
93+
selector: policyQualificationSelector,
94+
});
95+
96+
const {singlePolicyID, isMemberOfMoreThanOnePolicy, validExpensePolicyID} = qualificationResult ?? {};
97+
98+
// Per-key lookup for the resolved policy (only fires when that specific policy changes)
99+
const resolvedPolicyID = validExpensePolicyID ?? singlePolicyID;
100+
const [resolvedPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${resolvedPolicyID}`);
101+
102+
// If an expense policy ID is provided and valid, prefer it over the active policy
103+
if (validExpensePolicyID) {
104+
return {policyForMovingExpensesID: validExpensePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false};
105+
}
106+
70107
if (activePolicy && (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(activePolicy)) && (!isTimeRequest || isTimeTrackingEnabled(activePolicy))) {
71108
return {policyForMovingExpensesID: activePolicyID, policyForMovingExpenses: activePolicy, shouldSelectPolicy: false};
72109
}
73110

74-
if (singleUserPolicy && !isMemberOfMoreThanOnePolicy) {
75-
return {policyForMovingExpensesID: singleUserPolicy.id, policyForMovingExpenses: singleUserPolicy, shouldSelectPolicy: false};
111+
if (singlePolicyID && !isMemberOfMoreThanOnePolicy) {
112+
return {policyForMovingExpensesID: singlePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false};
76113
}
77114

78115
if (isMemberOfMoreThanOnePolicy) {

src/hooks/useReportAttributes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,18 @@ function useReportAttributes() {
1414
return reportAttributes?.reports;
1515
}
1616

17+
/**
18+
* Returns a single report's attributes using a selector.
19+
* Deep comparison is cheap (single small object), so re-renders only occur
20+
* when that specific report's attributes change — not on every global report change.
21+
*/
22+
function useReportAttributesByID(reportID: string | undefined) {
23+
const reportAttributesByIDSelector = (value: {reports?: Record<string, unknown>} | undefined) => (reportID ? value?.reports?.[reportID] : undefined);
24+
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {
25+
selector: reportAttributesByIDSelector,
26+
});
27+
return reportAttributes;
28+
}
29+
1730
export default useReportAttributes;
31+
export {useReportAttributesByID};

src/hooks/useReportIsArchived.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import {isArchivedReport} from '@libs/ReportUtils';
22
import ONYXKEYS from '@src/ONYXKEYS';
33
import useOnyx from './useOnyx';
44

5+
const isArchivedSelector = isArchivedReport;
6+
57
function useReportIsArchived(reportID?: string): boolean {
6-
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`);
7-
const isReportArchived = isArchivedReport(reportNameValuePairs);
8-
return isReportArchived;
8+
const [isArchived] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {
9+
selector: isArchivedSelector,
10+
});
11+
return !!isArchived;
912
}
1013

1114
export default useReportIsArchived;

src/hooks/useSearchBulkActions.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,17 +1029,19 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
10291029
shouldCallAfterModalHide: true,
10301030
});
10311031

1032-
for (const template of exportTemplates) {
1033-
exportOptions.push({
1034-
text: template.name,
1035-
icon: expensifyIcons.Table,
1036-
description: template.description,
1037-
onSelected: () => {
1038-
beginExportWithTemplate(template.templateName, template.type, template.policyID);
1039-
},
1040-
shouldCloseModalOnSelect: true,
1041-
shouldCallAfterModalHide: true,
1042-
});
1032+
if (!allSelectedAreDeleted) {
1033+
for (const template of exportTemplates) {
1034+
exportOptions.push({
1035+
text: template.name,
1036+
icon: expensifyIcons.Table,
1037+
description: template.description,
1038+
onSelected: () => {
1039+
beginExportWithTemplate(template.templateName, template.type, template.policyID);
1040+
},
1041+
shouldCloseModalOnSelect: true,
1042+
shouldCallAfterModalHide: true,
1043+
});
1044+
}
10431045
}
10441046

10451047
return exportOptions;

0 commit comments

Comments
 (0)