Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f84ffac
Add bulk export to accounting integration
huult Feb 4, 2026
68357b5
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 4, 2026
c5fc2ba
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 5, 2026
a5fb04c
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 5, 2026
ab27bca
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 6, 2026
921b1e7
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 9, 2026
6a22d1b
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 10, 2026
86564bc
Handle already exported reports
huult Feb 10, 2026
8ae2e17
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 10, 2026
855ea2c
Clear selected reports and handle draft report case
huult Feb 10, 2026
21a5285
Handle offline case when exporting to QBO
huult Feb 10, 2026
6292314
update default account ID
huult Feb 10, 2026
f4adc4f
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 11, 2026
467d65c
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 12, 2026
9d14f79
Add in-progress modal shown after already exported modal
huult Feb 12, 2026
d085b81
Add report contains export errors
huult Feb 12, 2026
effb7bd
update translation
huult Feb 12, 2026
6ca381b
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 12, 2026
5e23dc2
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 13, 2026
dfc087f
Revert showing export failed error message
huult Feb 13, 2026
0b2ffbe
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 14, 2026
db7eee1
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 16, 2026
2e75d31
Remove in-progress modal and update report name display
huult Feb 16, 2026
bea4dbf
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 18, 2026
dc89de4
Remove unused translations and add prop for integration icon
huult Feb 18, 2026
63f1f9f
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 19, 2026
f51ae99
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 21, 2026
e4b7ec1
Remove duplicate code and update optimistic data
huult Feb 21, 2026
a0ba451
Use snapshot data to get report actions
huult Feb 21, 2026
87e8a1f
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 23, 2026
4942592
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 23, 2026
5820af7
Update logic to get exported reports
huult Feb 23, 2026
205976a
Remove unused imports
huult Feb 23, 2026
d49e604
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 23, 2026
19396a0
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 24, 2026
3dde75b
Use report ID as array param when exporting report
huult Feb 24, 2026
f70e68e
Update default report ID
huult Feb 24, 2026
de48a87
run prettier
huult Feb 24, 2026
fa3cbc0
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 24, 2026
45efd63
Merge remote-tracking branch 'upstream/main' into 79515-add-bulk-expo…
huult Feb 25, 2026
ee6ecf1
update dependencies
huult Feb 25, 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
4 changes: 2 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@
if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
exportToIntegration(moneyRequestReport?.reportID, connectedIntegration);
} else if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration);
markAsManuallyExported([moneyRequestReport?.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration);
}
}, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]);

Expand Down Expand Up @@ -989,7 +989,7 @@
setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED);
return;
}
markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration);
markAsManuallyExported([moneyRequestReport?.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration);
},
},
};
Expand Down Expand Up @@ -1745,7 +1745,7 @@
}
return option;
});
}, [originalSelectedTransactionsOptions, showDeleteModal, dismissedRejectUseExplanation]);

Check warning on line 1748 in src/components/MoneyReportHeader.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useMemo has missing dependencies: 'isDelegateAccessRestricted' and 'showDelegateNoAccessModal'. Either include them or remove the dependency array

Check warning on line 1748 in src/components/MoneyReportHeader.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useMemo has missing dependencies: 'isDelegateAccessRestricted' and 'showDelegateNoAccessModal'. Either include them or remove the dependency array

const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function ExportWithDropdownMenu({
if (exportType === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
exportToIntegration(reportID, connectionName);
} else if (exportType === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
markAsManuallyExported(reportID, connectionName);
markAsManuallyExported([reportID], connectionName);
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/parameters/ReportExportParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';

type ReportExportParams = {
reportIDList: string;
reportIDList: string | string[];
connectionName: ValueOf<typeof CONST.POLICY.CONNECTIONS.NAME>;
type: 'MANUAL';
/**
Expand Down
47 changes: 25 additions & 22 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@
/** @deprecated This value is deprecated and will be removed soon after migration. Use the email from useCurrentUserPersonalDetails hook instead. */
let deprecatedCurrentUserLogin: string | undefined;

Onyx.connect({

Check warning on line 317 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -328,7 +328,7 @@
},
});

Onyx.connect({

Check warning on line 331 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeReportIDOnyxConnect = value),
});
Expand All @@ -336,7 +336,7 @@
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 339 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
if (!key || !actions) {
Expand All @@ -348,7 +348,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 351 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -357,7 +357,7 @@
});

let allPersonalDetails: OnyxEntry<PersonalDetailsList> = {};
Onyx.connect({

Check warning on line 360 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand All @@ -372,7 +372,7 @@
});

let onboarding: OnyxEntry<Onboarding>;
Onyx.connect({

Check warning on line 375 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ONBOARDING,
callback: (val) => {
if (Array.isArray(val)) {
Expand All @@ -383,7 +383,7 @@
});

let deprecatedIntroSelected: OnyxEntry<IntroSelected> = {};
Onyx.connect({

Check warning on line 386 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_INTRO_SELECTED,
callback: (val) => (deprecatedIntroSelected = val),
});
Expand Down Expand Up @@ -4958,54 +4958,57 @@
API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData});
}

function markAsManuallyExported(reportID: string, connectionName: ConnectionName) {
const action = buildOptimisticExportIntegrationAction(connectionName, true);
function markAsManuallyExported(reportIDs: string[], connectionName: ConnectionName) {
const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName];
const optimisticReportActionID = action.reportActionID;

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
{
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [];
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [];
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [];
const reportData: Array<{reportID: string; label: string; optimisticReportActionID: string}> = [];

// Process each report ID
for (const reportID of reportIDs) {
const action = buildOptimisticExportIntegrationAction(connectionName, true);
const optimisticReportActionID = action.reportActionID;

reportData.push({
reportID,
label,
optimisticReportActionID,
});

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: action,
},
},
];
});

const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
{
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: {
pendingAction: null,
},
},
},
];
});

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
{
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: {
errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
},
];
});
}

const params = {
markedManually: true,
data: JSON.stringify([
{
reportID,
label,
optimisticReportActionID,
},
]),
data: JSON.stringify(reportData),
} satisfies MarkAsExportedParams;

API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData});
Expand Down
100 changes: 53 additions & 47 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ function handleActionButtonPress({
return;
}

exportToIntegrationOnSearch(hash, item?.reportID, connectedIntegration, currentSearchKey);
if (!item?.reportID) {
return;
}

exportToIntegrationOnSearch(hash, [item.reportID], connectedIntegration, currentSearchKey);
return;
}
default:
Expand Down Expand Up @@ -644,87 +648,89 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], curre
API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData});
}

function exportToIntegrationOnSearch(hash: number, reportID: string | undefined, connectionName: ConnectionName, currentSearchKey?: SearchKey) {
if (!reportID) {
function exportToIntegrationOnSearch(hash: number, reportIDs: string[], connectionName: ConnectionName, currentSearchKey?: SearchKey) {
if (!reportIDs.length) {
return;
}
const optimisticAction = buildOptimisticExportIntegrationAction(connectionName);
const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null};
const optimisticReportActionID = optimisticAction.reportActionID;
const optimisticReportActions: Record<string, string> = {};

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {isActionLoading: true},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: optimisticAction,
},
onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: true}])),
},
];

const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [];

const finallyData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {isActionLoading: false},
onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])),
},
{
];

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [];

for (const reportID of reportIDs) {
const optimisticAction = buildOptimisticExportIntegrationAction(connectionName);
const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null};
const optimisticReportActionID = optimisticAction.reportActionID;

optimisticReportActions[reportID] = optimisticReportActionID;

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: successAction,
[optimisticReportActionID]: optimisticAction,
},
},
];
});

// If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search
if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
data: {
[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null,
},
[optimisticReportActionID]: successAction,
},
});
}

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {isActionLoading: false},
},
{
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[optimisticReportActionID]: null,
},
},
{
});

failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
},
];
});
}

// If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search
if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
value: {
data: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])),
},
});
}

const params = {
reportIDList: reportID,
reportIDList: reportIDs,
connectionName,
type: 'MANUAL',
optimisticReportActions: JSON.stringify({
[reportID]: optimisticReportActionID,
}),
optimisticReportActions: JSON.stringify(optimisticReportActions),
} satisfies ReportExportParams;

API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData});
API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, successData, failureData, finallyData});
}

function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], currentSearchKey?: SearchKey) {
Expand Down
Loading
Loading