Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5c80f02
[Duplicate Expense] Allow bulk duplication.
Krishna2323 Mar 10, 2026
0a910f3
add tests.
Krishna2323 Mar 10, 2026
17b8a60
refactor code.
Krishna2323 Mar 10, 2026
cff4f62
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 10, 2026
e2b5c55
minor updates.
Krishna2323 Mar 10, 2026
2dae542
fix optimistic action issue.
Krishna2323 Mar 10, 2026
5760d55
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 10, 2026
9591889
fix tests and ESLint.
Krishna2323 Mar 10, 2026
ab91ed8
fix tests.
Krishna2323 Mar 10, 2026
c0b25f5
add more tests.
Krishna2323 Mar 10, 2026
2de7466
add missing translations.
Krishna2323 Mar 10, 2026
55447e2
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 13, 2026
0bb2736
fix prettier.
Krishna2323 Mar 13, 2026
3b6a21e
Add submitter and Per Diem date filters to bulk duplicate option
Krishna2323 Mar 14, 2026
dca96c3
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Mar 15, 2026
fe9b4e0
address codex review comments.
Krishna2323 Mar 15, 2026
d2c9894
update tests.
Krishna2323 Mar 15, 2026
c2150c8
perf: defer bulk-duplicate Onyx subscriptions until the option is vis…
Krishna2323 Mar 17, 2026
d2802aa
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 17, 2026
d6e1f5d
perf: hoist searchReports computation outside the .every() loop.
Krishna2323 Mar 17, 2026
9036e5c
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Mar 18, 2026
370c6e3
fix: resolve source reports from search data instead of only Onyx.
Krishna2323 Mar 18, 2026
556354f
fix: carry forward iouReportID across bulk-duplicate loop iterations.
Krishna2323 Mar 18, 2026
f59ca89
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Mar 20, 2026
7f485d8
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 22, 2026
01cc84f
fix ESLint.
Krishna2323 Mar 22, 2026
e3ec590
fix: address review comments — plural text, displayName, production gate
Krishna2323 Mar 23, 2026
1f1e7b3
update translations.
Krishna2323 Mar 23, 2026
b96226c
fix translations.
Krishna2323 Mar 23, 2026
ecd002e
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 25, 2026
7546137
add bulk duplicate expense option on report details page.
Krishna2323 Mar 25, 2026
e4a450b
clear selection on duplicate.
Krishna2323 Mar 25, 2026
6e10ed9
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Mar 27, 2026
7cd5378
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 29, 2026
db0da62
fix prettier and lint.
Krishna2323 Mar 29, 2026
efa3ce3
Remove manual useMemo/useCallback from React Compiler-compatible hooks
Krishna2323 Mar 29, 2026
d003789
Reuse BulkDuplicateHandler on report details page to avoid unconditio…
Krishna2323 Mar 29, 2026
255d93d
Fix duplicate handler not firing from MoneyReportHeader and remove de…
Krishna2323 Mar 30, 2026
aaed1b8
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 30, 2026
a124b9c
fix TS.
Krishna2323 Mar 30, 2026
5bbe3b6
Memoize duplicate-related computations in useSelectedTransactionsActions
Krishna2323 Mar 30, 2026
b84f459
fix TS.
Krishna2323 Mar 30, 2026
6551b4d
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Mar 31, 2026
77cb659
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Apr 2, 2026
09c81ea
Suppress per-item sound and defer auto-submit in bulkDuplicateExpenses
Krishna2323 Apr 2, 2026
d3be3f3
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Apr 3, 2026
0ce86c5
Move bulk action type constants to CONST.REPORT and annotate eslint s…
Krishna2323 Apr 6, 2026
f40173f
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Apr 6, 2026
3542312
remove duplicate dependency.
Krishna2323 Apr 6, 2026
0d7a532
fix tests.
Krishna2323 Apr 7, 2026
2d3c64e
Merge branch 'Expensify:main' into krishna2323/issue/84281
Krishna2323 Apr 7, 2026
3ad6703
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Apr 8, 2026
fe9eb48
Merge branch 'main' into krishna2323/issue/84281
Krishna2323 Apr 9, 2026
0a2f1b0
fix: remove stale shouldShowMoreContent block from merge conflict res…
Krishna2323 Apr 9, 2026
af30acd
fix: show bulk duplicate option for unreported expenses
Krishna2323 Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7367,6 +7367,7 @@ const CONST = {
REJECT: 'reject',
CHANGE_REPORT: 'changeReport',
SPLIT: 'split',
DUPLICATE: 'duplicate',
UNDELETE: 'undelete',
},
TRANSACTION_TYPE: {
Expand Down
455 changes: 236 additions & 219 deletions src/components/MoneyReportHeader.tsx

Large diffs are not rendered by default.

285 changes: 152 additions & 133 deletions src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions src/components/Search/BulkDuplicateHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {useEffect} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import useBulkDuplicateAction from '@hooks/useBulkDuplicateAction';
import type {Report, Transaction} from '@src/types/onyx';

type BulkDuplicateHandlerProps = {
selectedTransactionsKeys: string[];
allTransactions: OnyxCollection<Transaction>;
allReports: OnyxCollection<Report> | undefined;
searchData: Record<string, unknown> | undefined;
onHandlerReady: (handler: () => void) => void;
onAfterDuplicate?: () => void;
};

/**
* Invisible component that subscribes to action-time Onyx data for bulk duplication.
* Only mounted when the duplicate option is visible, avoiding unnecessary global
* subscriptions on the search page for users who aren't duplicating.
*/
function BulkDuplicateHandler({selectedTransactionsKeys, allTransactions, allReports, searchData, onHandlerReady, onAfterDuplicate}: BulkDuplicateHandlerProps) {
const handleDuplicate = useBulkDuplicateAction({selectedTransactionsKeys, allTransactions, allReports, searchData, onAfterDuplicate});

useEffect(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BulkDuplicateHandler is a performance optimization. The actual duplication logic (useBulkDuplicateAction) requires 13+ Onyx subscriptions. This component is only mounted when isDuplicateOptionVisible is true, so those subscriptions don't exist for users who aren't actively duplicating. It renders null — its only job is to call useBulkDuplicateAction and pass the resulting handler back to the parent via onHandlerReady. This pattern matches the lazy-mount approach already used on the search page.

onHandlerReady(handleDuplicate);
}, [handleDuplicate, onHandlerReady]);

return null;
}

export default BulkDuplicateHandler;
15 changes: 15 additions & 0 deletions src/components/Search/SearchBulkActionsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BulkDuplicateHandler from './BulkDuplicateHandler';
import {useSearchActionsContext, useSearchStateContext} from './SearchContext';
import type {BulkPaySelectionData, SearchQueryJSON} from './types';

Expand Down Expand Up @@ -66,6 +67,11 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
handleDownloadErrorModalClose,
dismissModalAndUpdateUseHold,
dismissRejectModalBasedOnAction,
isDuplicateOptionVisible,
setDuplicateHandler,
allTransactions,
allReports,
searchData,
} = useSearchBulkActions({queryJSON});

const currentSelectedPolicyID = selectedPolicyIDs?.at(0);
Expand Down Expand Up @@ -101,6 +107,15 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {

return (
<>
{isDuplicateOptionVisible && (
<BulkDuplicateHandler
selectedTransactionsKeys={selectedTransactionsKeys}
allTransactions={allTransactions}
allReports={allReports}
searchData={searchData}
onHandlerReady={setDuplicateHandler}
/>
)}
<KYCWall
ref={kycWallRef}
chatReportID={currentSelectedReportID}
Expand Down
93 changes: 93 additions & 0 deletions src/hooks/useBulkDuplicateAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {hasSeenTourSelector} from '@selectors/Onboarding';
import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {useSearchActionsContext} from '@components/Search/SearchContext';
import {bulkDuplicateExpenses} from '@libs/actions/IOU/Duplicate';
import {getPolicyExpenseChat} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, Transaction} from '@src/types/onyx';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useDefaultExpensePolicy from './useDefaultExpensePolicy';
import useOnyx from './useOnyx';
import usePermissions from './usePermissions';

type UseBulkDuplicateActionParams = {
selectedTransactionsKeys: string[];
allTransactions: OnyxCollection<Transaction>;
allReports: OnyxCollection<Report> | undefined;
searchData: Record<string, unknown> | undefined;
onAfterDuplicate?: () => void;
};

/**
* Hook that subscribes to action-time-only Onyx data needed for bulk expense duplication.
* Designed to be called inside a component that only mounts when the duplicate option is visible,
* so these subscriptions don't exist for users who aren't actively duplicating.
*/
function useBulkDuplicateAction({selectedTransactionsKeys, allTransactions, allReports, searchData, onAfterDuplicate}: UseBulkDuplicateActionParams) {
const {accountID} = useCurrentUserPersonalDetails();
const {clearSelectedTransactions} = useSearchActionsContext();
const defaultExpensePolicy = useDefaultExpensePolicy();
const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);

const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES);
const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
const [transactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftsSelector});
const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
const [betas] = useOnyx(ONYXKEYS.BETAS);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS);
const [targetPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${defaultExpensePolicy?.id}`);
const [targetPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy?.id}`);

const sourcePolicyIDMap: Record<string, string | undefined> = {};
for (const transactionID of selectedTransactionsKeys) {
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const reportID = transaction?.reportID;
if (!reportID) {
continue;
}
const report = (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] as Report | undefined) ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
sourcePolicyIDMap[transactionID] = report?.policyID;
}

const handleDuplicate = () => {
const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id);

bulkDuplicateExpenses({
transactionIDs: selectedTransactionsKeys,
allTransactions: allTransactions ?? {},
sourcePolicyIDMap,
targetPolicy: (defaultExpensePolicy ?? undefined) as OnyxEntry<Policy>,
targetPolicyCategories: targetPolicyCategories ?? {},
targetPolicyTags: targetPolicyTags ?? {},
targetReport: activePolicyExpenseChat,
personalDetails,
isASAPSubmitBetaEnabled,
introSelected,
activePolicyID,
quickAction,
policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
isSelfTourViewed,
transactionDrafts,
draftTransactionIDs,
betas,
recentWaypoints,
});

if (onAfterDuplicate) {
onAfterDuplicate();
} else {
clearSelectedTransactions(undefined, true);
}
};

return handleDuplicate;
}

export default useBulkDuplicateAction;
Loading
Loading