Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/hooks/useBulkDuplicateReportAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {useSearchSelectionActions} from '@components/Search/SearchContext';
import type {SelectedReports} from '@components/Search/types';

import {bulkDuplicateReports} from '@libs/actions/IOU/Duplicate';
import Log from '@libs/Log';
import {getPolicyExpenseChat} from '@libs/ReportUtils';

import CONST from '@src/CONST';
Expand Down Expand Up @@ -80,6 +81,8 @@ function useBulkDuplicateReportAction({selectedReports, allReports, searchData}:
formatPhoneNumber,
getCurrencyDecimals,
conciergeChat,
}).catch((error: unknown) => {
Log.warn('[useBulkDuplicateReportAction] Failed to duplicate the selected reports', {error});
});

clearSelectedTransactions(undefined, true);
Expand Down
24 changes: 22 additions & 2 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import type {PerDiemExpenseInformation} from './PerDiem';
import type {CreateDistanceRequestInformation} from './Split';
import type {CreateTrackExpenseParams} from './TrackExpense';

import {getAllReports, getAllTransactions} from '.';
import {getAllReports, getAllTransactions, getCurrentUserAccountIDFromSession} from '.';
import {getCleanUpTransactionThreadReportOnyxData} from './DeleteMoneyRequest';
import {getMoneyRequestParticipantsFromReport} from './MoneyRequest';
import {submitPerDiemExpense} from './PerDiem';
Expand Down Expand Up @@ -1352,7 +1352,7 @@ type BulkDuplicateReportsParams = {
conciergeChat: OnyxEntry<OnyxTypes.Report>;
};

function bulkDuplicateReports({
async function bulkDuplicateReports({
dateFnsLocale,
selectedReports: selectedReportsParam,
allReports,
Expand Down Expand Up @@ -1404,6 +1404,9 @@ function bulkDuplicateReports({
transactionsByReportID.set(transaction.reportID, list);
}

let hasDuplicatedReport = false;
const accountIDAtStart = getCurrentUserAccountIDFromSession();

for (const selectedReport of selectedReportsParam) {
const reportID = selectedReport.reportID;
if (!reportID) {
Expand All @@ -1420,6 +1423,21 @@ function bulkDuplicateReports({
continue;
}

if (hasDuplicatedReport) {
// Temporary until the backend exposes a single command that duplicates a whole selection. Until then,
// let the previous report's optimistic writes apply before blocking the thread again.
// eslint-disable-next-line no-await-in-loop
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
Comment thread
mukhrr marked this conversation as resolved.

// Signing out clears Onyx, so the reports and policies captured above belong to an account that is no
// longer active. Duplicating the rest of them would write the previous account's data under the new one.
if (getCurrentUserAccountIDFromSession() !== accountIDAtStart) {
return;
}
}

const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];
const isSourcePolicyValid = !!reportPolicy && isPolicyAccessible(reportPolicy, currentUserLogin);
const chatReportID = report.chatReportID ?? report.parentReportID;
Expand Down Expand Up @@ -1464,6 +1482,8 @@ function bulkDuplicateReports({
participantsPolicyTags,
conciergeChat,
});

hasDuplicatedReport = true;
}

playSound(SOUNDS.DONE);
Expand Down
5 changes: 5 additions & 0 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ function getCurrentUserPersonalDetails(): OnyxEntry<OnyxTypes.PersonalDetails> {
return deprecatedCurrentUserPersonalDetails;
}

function getCurrentUserAccountIDFromSession(): number {
return deprecatedUserAccountID;
}

function getRecentAttendees(): OnyxEntry<Attendee[]> {
return recentAttendees;
}
Expand All @@ -191,6 +195,7 @@ export {
getAllReportNameValuePairs,
getAllTransactionDrafts,
getCurrentUserPersonalDetails,
getCurrentUserAccountIDFromSession,
getRecentAttendees,
getAllSnapshots,
getSearchQueryByHash,
Expand Down
81 changes: 71 additions & 10 deletions tests/actions/IOUTest/DuplicateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3623,13 +3623,74 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'rpt2'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'rpt2'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(2);
expect(countWriteCommandCalls(WRITE_COMMANDS.REQUEST_MONEY)).toBe(2);
});

it('should not duplicate every selected report in a single synchronous pass', async () => {
const reportIDs = ['rpt1', 'rpt2', 'rpt3'];
const allReports: Record<string, Report> = {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

for (const reportID of reportIDs) {
allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {
reportID,
policyID: SOURCE_POLICY_ID,
ownerAccountID: RORY_ACCOUNT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
reportName: `Report ${reportID}`,
chatReportID: ACTIVE_PEC_REPORT_ID,
};
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}tx-${reportID}`, createCashTransaction(`1${reportID.slice(-1)}`, reportID));
}

const duplicating = bulkDuplicateReports(getDefaultBulkParams(reportIDs, {allReports}));

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
expect(countWriteCommandCalls(WRITE_COMMANDS.REQUEST_MONEY)).toBe(1);

await duplicating;
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(3);
expect(countWriteCommandCalls(WRITE_COMMANDS.REQUEST_MONEY)).toBe(3);
});

it('should stop duplicating the remaining reports when the account changes mid-flight', async () => {
await Onyx.merge(ONYXKEYS.SESSION, {accountID: RORY_ACCOUNT_ID});
await waitForBatchedUpdates();

const reportIDs = ['rpt1', 'rpt2', 'rpt3'];
const allReports: Record<string, Report> = {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};
for (const reportID of reportIDs) {
allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {
reportID,
policyID: SOURCE_POLICY_ID,
ownerAccountID: RORY_ACCOUNT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
reportName: `Report ${reportID}`,
chatReportID: ACTIVE_PEC_REPORT_ID,
};
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}tx-${reportID}`, createCashTransaction(`1${reportID.slice(-1)}`, reportID));
}

const duplicating = bulkDuplicateReports(getDefaultBulkParams(reportIDs, {allReports}));
expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);

await Onyx.merge(ONYXKEYS.SESSION, {accountID: RORY_ACCOUNT_ID + 1});
await waitForBatchedUpdates();
await duplicating;

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
expect(countWriteCommandCalls(WRITE_COMMANDS.REQUEST_MONEY)).toBe(1);
});

it('should use source policy when accessible, and fall back to default policy when not', async () => {
const chatForSource: Report = {
reportID: 'chatSource',
Expand Down Expand Up @@ -3671,7 +3732,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(
await bulkDuplicateReports(
getDefaultBulkParams(['rpt1', 'rpt2'], {
allReports,
allPolicyCategories: {
Expand Down Expand Up @@ -3715,7 +3776,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand All @@ -3740,7 +3801,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand Down Expand Up @@ -3776,7 +3837,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand Down Expand Up @@ -3804,7 +3865,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'nonexistent1', 'nonexistent2'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'nonexistent1', 'nonexistent2'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand All @@ -3826,7 +3887,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand Down Expand Up @@ -3864,7 +3925,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'rpt2'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1', 'rpt2'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(2);
Expand Down Expand Up @@ -3918,7 +3979,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(
await bulkDuplicateReports(
getDefaultBulkParams(['rpt1', 'rpt2'], {
allReports,
allPolicyTags: {
Expand Down Expand Up @@ -3958,7 +4019,7 @@ describe('actions/Duplicate', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${ACTIVE_PEC_REPORT_ID}`]: activePolicyExpenseChat,
};

bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await bulkDuplicateReports(getDefaultBulkParams(['rpt1'], {allReports}));
await waitForBatchedUpdates();

expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(1);
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/hooks/useBulkDuplicateReportActionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Onyx from 'react-native-onyx';
import createMock from '../../utils/createMock';

jest.mock('@libs/actions/IOU/Duplicate', () => ({
bulkDuplicateReports: jest.fn(),
bulkDuplicateReports: jest.fn(() => Promise.resolve()),
}));

jest.mock('@hooks/useLocalize', () => ({
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import createMock from '../../utils/createMock';

jest.mock('@libs/actions/IOU/Duplicate', () => ({
bulkDuplicateExpenses: jest.fn(),
bulkDuplicateReports: jest.fn(),
bulkDuplicateReports: jest.fn(() => Promise.resolve()),
}));

jest.mock('@libs/actions/Search', () => ({
Expand Down
Loading