Skip to content

Group bulk export options by integration for multi-workspace selections - #96728

Open
MelvinBot wants to merge 8 commits into
mainfrom
claude-bulkExportMultiWorkspaceIntegration
Open

Group bulk export options by integration for multi-workspace selections#96728
MelvinBot wants to merge 8 commits into
mainfrom
claude-bulkExportMultiWorkspaceIntegration

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reference: Slack thread

Explanation of Change

Bulk integration export options (export to accounting + "Mark as exported") only appeared when every selected report belonged to a single workspace, because getExportOptions() in useSearchBulkActions derived a single connectedIntegration from selectedPolicyIDs.length === 1. When a selection spanned multiple workspaces (e.g. one QBO report + one Sage Intacct report), policy was undefined, so no integration options were shown at all — even though each report was individually exportable. This matched neither Classic Expensify nor user expectations.

This PR groups the selected reports by their connected accounting integration and builds one "Export to " option and one "Mark as exported" option per integration, each scoped to just the reports for that integration. Export options are listed first, followed by the mark-as-exported options. A single-workspace selection collapses to one group, so the previous behavior is unchanged.

🤖 This is an automated draft PR created by MelvinBot at the request of ahmedGaber93, implementing the plan in this comment.

AI checks run locally (by MelvinBot):

  • npm test tests/unit/hooks/useSearchBulkActionsExportTest.ts — ✅ pass (3 tests, includes a new multi-workspace case)
  • npm run typecheck-tsgo — ✅ pass
  • npm run lint-changed — ✅ pass
  • npm run fmt (oxfmt) — ✅ applied, only the two changed files reformatted
  • npm run react-compiler-compliance-check check src/hooks/useSearchBulkActions.ts — no new divergence: the reported ref-access errors are pre-existing (identical error set on origin/main; the file already carries a react-hooks/refs eslint-disable)

Fixed Issues

$ #95820
PROPOSAL: #95820 (comment)

Tests

Precondition

  • Create Workspace A and connect it to an accounting integration (e.g. QuickBooks Online).
  • Create Workspace B and connect it to a different accounting integration (e.g. Sage Intacct).
  • Create several reports in both workspaces with Draft and Approved statuses.
  • Navigate to Spend > Reports.

Case 1: Export reports from a single workspace

  1. Select:
    • 1 Approved report from Workspace A
    • 1 Draft report from Workspace A
  2. Verify the bulk export menu displays:
    • Export to QuickBooks Online
    • Mark as exported (QuickBooks Online)
  3. Click Export to QuickBooks Online.
  4. Verify the confirmation modal appears with:
    • Title: Export 1/2 reports to QuickBooks Online?
    • Message: Only reports that are eligible to export will be exported.
  5. Click Export 1 report.
  6. Open the approved report and verify it displays the message:
    • Exported to QuickBooks Online and ...
  7. Repeat steps 1–3.
  8. Verify another confirmation modal appears with:
    • Title: Careful!
    • Message: The following reports have already been exported to QuickBooks Online. Are you sure ...
  9. Click Yes, export again and verify the report is exported again.
  10. Repeat the entire flow using Mark as exported (QuickBooks Online).

Case 2: Export reports from multiple workspaces

  1. Select:
    • 1 Approved report from Workspace A
    • 1 Approved report from Workspace B
  2. Verify the bulk export menu displays:
    • Export to QuickBooks Online
    • Export to Sage Intacct
    • Mark as exported (QuickBooks Online)
    • Mark as exported (Sage Intacct)
  3. Click Export to QuickBooks Online.
  4. Verify the confirmation modal appears with:
    • Title: Export 1/2 reports to QuickBooks Online?
    • Message: Only reports connected to QuickBooks Online will be exported.
  5. Click Export 1 report.
  6. Open the approved report from Workspace A and verify it displays the message:
    • Exported to QuickBooks Online and ...
  7. Repeat the entire flow using Mark as exported (QuickBooks Online).

Case 3: Mixed eligibility across workspaces

  1. Select:
    • 1 Approved report from Workspace A
    • 1 Draft report from Workspace A
    • 1 Approved report from Workspace B
  2. Verify the bulk export menu displays:
    • Export to QuickBooks Online
    • Export to Sage Intacct
    • Mark as exported (QuickBooks Online)
    • Mark as exported (Sage Intacct)
  3. Click Export to QuickBooks Online.
  4. Verify the confirmation modal appears with:
    • Title: Export 1/3 reports to QuickBooks Online?

    • Message:

      Only reports connected to QuickBooks Online will be exported.
      Only reports that are eligible to export will be exported.
      
  5. Click Export 1 report.
  6. Open the approved report from Workspace A and verify it displays the message:
    • Exported to QuickBooks Online and ...
  7. Repeat the entire flow using Mark as exported (QuickBooks Online).
  • Verify that no errors appear in the JS console

Offline tests

N/A.

  • Verify that no errors appear in the JS console

QA Steps

Same as Test step

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team July 22, 2026 11:03
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-bulkExportMultiWorkspaceIntegration
git checkout -b claude-bulkExportMultiWorkspaceIntegration upstream/claude-bulkExportMultiWorkspaceIntegration
git push -u origin claude-bulkExportMultiWorkspaceIntegration

Then you can close this PR and open a new one from your fork.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 I checked all the checks on this PR. No code fix is needed — every real CI check passes.

The only failing check is PR Author Checklist / checklist, and it fails for a non-code reason:

You completed 0 out of 38 checklist items with 38 unfinished items
##[error]PR Author Checklist is not completely filled out.

This is expected for a draft PR: the Tests/QA sections still have // TODO: The human co-author must fill out... placeholders, and none of the 38 author-checklist boxes are ticked. That check only turns green once the human co-author fills out the testing steps and checks the boxes before marking the PR Ready for review — it's not something I should (or can) resolve by pushing code.

Everything else is green: typecheck, all 8 test jobs, ESLint, Bun tests, Storybook tests, React Compiler Compliance, Oxfmt, spellcheck, all build jobs, and the perf tests (baseline-perf-tests, branch-perf-tests, validate-perf-tests) all pass.

Next step: ahmedGaber93 (co-author), please fill in the Tests and QA sections and complete the author checklist, then mark the PR ready for review — the checklist check will pass at that point.

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/ConfirmModal.tsx 100.00% <ø> (ø)
src/components/ConfirmContent.tsx 88.70% <66.66%> (-1.13%) ⬇️
src/hooks/useSearchBulkActions.ts 57.33% <95.91%> (+3.20%) ⬆️
... and 102 files with indirect coverage changes

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Update the bulk action sorting to group each integration's actions together:

  • Export to Integration1
  • Mark as exported (Integration1)
  • Export to Integration2
  • Mark as exported (Integration2)

instead of:

  • Export to Integration1
  • Export to Integration2
  • Mark as exported (Integration1)
  • Mark as exported (Integration2)
Screenshot 2026-07-23 at 11 16 39 AM

…ether

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — pushed cdbe299.

The two loops in getExportOptions() (all "Export to X" first, then all "Mark as exported") are now merged into a single pass over reportsByIntegration, so each integration's actions are grouped:

  • Export to Integration1
  • Mark as exported (Integration1)
  • Export to Integration2
  • Mark as exported (Integration2)

See src/hooks/useSearchBulkActions.ts:1572-1606. The per-integration canExport/canMarkAsExported checks and report scoping are unchanged, and a single-workspace selection still collapses to one group.

Also extended the multi-workspace unit test to assert the new grouped ordering.

AI checks run locally:

  • npm test tests/unit/hooks/useSearchBulkActionsExportTest.ts — ✅ pass (3 tests, including the updated ordering assertion)
  • typecheck / lint-changed / oxfmt⚠️ could not run in this sandbox (the local tsc/tsgo and formatter binaries aren't resolvable here). This change is type/lint/format-neutral: no new imports, types, or signatures — it only reorders two existing loops into one using the same object literals — so CI's checks on this push will be the source of truth for those gates.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I analyzed the failing checks — there's no code failure to fix here.

The only failing check is PR Author Checklist / checklist, and it's not a code/CI test. It fails because the manual checklist hasn't been filled out:

You completed 0 out of 38 checklist items with 38 unfinished items
##[error]PR Author Checklist is not completely filled out.

Since this is an automated draft PR, the checklist boxes and the // TODO: Tests / QA Steps sections still need to be completed by the human co-author (ahmedGaber93) before this check will pass. That's expected for a draft and can't be resolved with a commit.

Every functional and AI check is green: typecheck, ESLint, Oxfmt, all 8 Jest test shards, Bun tests, Storybook tests, React Compiler Compliance, validate, and all builds. The two *-perf-tests are still running — they're informational performance benchmarks, not merge-gating.

I didn't push any change because there's nothing broken in the code.

@ahmedGaber93

ahmedGaber93 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot Please update with the following changes.

1) Add a partial-export confirmation modal

When a selection spans multiple integrations, clicking "Export to " only exports the subset of reports connected to that integration. Before exporting, if it's a partial export (the integration's report count is less than the total number of selected reports), show a confirmation modal:

  • Title: "Export {exportableCount}/{selectedCount} reports to {Integration}?"
  • Description: "Only reports connected to {Integration} will be exported. The rest of your selection belongs to other integrations and will be skipped."
  • Confirm button: "Export {exportableCount} report(s)"
  • Cancel button: "Cancel"

2) Show BOTH confirmations in sequence when applicable

If the chosen integration's subset ALSO contains reports that were already exported, show two confirmations in sequence (never combined):

  1. First the partial-export modal above.
  2. Then the existing "export again" modal (workspace.exportAgainModal).

Confirming both proceeds with the export; cancelling either one aborts. Present them sequentially (present the second only after the first resolves) so they don't overlap.

3) Apply the SAME flow to "Mark as exported"

Route mark-as-exported through the same shared confirmation flow used by export so the behavior stays identical.

4) Tests

Update tests/unit/hooks/useSearchBulkActionsExportTest.ts to cover:

  • Per-integration options for multi-workspace selections.
  • Partial export shows the partial modal, then exports the subset.
  • Partial + already-exported shows the partial modal first, then the export-again modal, then exports.
  • Single-integration already-exported shows the export-again modal.
  • Already-exported detection works from report actions even when isExportedToIntegration is stale/false.
  • No partial modal when every selected report belongs to the chosen integration.
  • Equivalent coverage for the "Mark as exported" partial + export-again flow.

Please run npm run fmt, npm run lint-changed, npm run typecheck, and the export unit tests before pushing.

- Show a partial-export modal when Export/Mark-as-exported to an integration
  only covers a subset of the multi-integration selection
- Present the partial modal and the existing export-again modal sequentially
- Detect already-exported reports from report actions (live Onyx first, then
  the search snapshot) instead of the stale isExportedToIntegration field
- Route Mark as exported through the same shared confirmation flow
- Cover the new flows in useSearchBulkActionsExportTest

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — pushed to claude-bulkExportMultiWorkspaceIntegration (ea3fcb9). All five changes are in, routed through one shared confirmation flow so Export and Mark as exported behave identically.

What changed

1 & 2 — Partial-export modal, shown in sequence with export-again
Both bulk actions now run through buildIntegrationHandleExportAction. A partial export is detected when the chosen integration's report count is less than the total selected reports. When partial, the partial-export modal is shown first; only after it resolves does the existing workspace.exportAgainModal appear (if the subset was already exported). They're presented one after another via the modal promise chain, never combined — cancelling either aborts.

New copy lives under workspace.exportPartialModal (title / description / plural confirmText / cancelText) with params ExportPartialModalTitleParams and ExportPartialModalDescriptionParams added to params.ts, translated across all 10 locale files.

3 — Already-exported detection from report actions
The detection loop now treats a report as exported when any of: liveReport/snapshotReport pendingFields.export, liveReport/snapshotReport isExportedToIntegration === true, or isExported(reportActions) passed only the report actions (so it evaluates the actions instead of short-circuiting on the stale field). Report actions are read from live Onyx first, falling back to the search snapshot (reportActions_<reportID>).

4 — Mark as exported uses the exact same buildIntegrationHandleExportAction, so it inherits the partial + export-again flow.

5 — TestsuseSearchBulkActionsExportTest.ts now covers per-integration options, partial-only, partial + export-again, single-integration export-again, stale-isExportedToIntegration detection via report actions, the no-modal case, cancel-aborts, and the Mark-as-exported partial + export-again flow.

Checks (run locally)

  • npm test tests/unit/hooks/useSearchBulkActionsExportTest.ts — ✅ 10/10 pass
  • npm run typecheck (tsc, CI gate) — ✅ 0 errors
  • npm run lint on changed files — ✅ clean
  • npm run fmt — ✅ applied
  • React Compiler compliance — no new divergence (the pre-existing refs errors are unchanged from origin/main: 14 → 14)

One note for the human co-author: the plural confirmText uses one/other forms in every locale; the non-English translations are my best-effort and worth a native-speaker glance before merge.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The only failing check is PR Author Checklist / checklist, and it's not a code problem — there's nothing for me to fix or push.

It failed with:

You completed 0 out of 38 checklist items with 38 unfinished items
PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

This is a human-only step: the PR Author Checklist must be filled out (and the Tests/QA Steps sections completed) by the human co-author before the PR is marked "Ready for review". I intentionally don't tick those boxes on your behalf, since each item is an attestation that a person reviewed/tested it.

ahmedGaber93, to clear this check: go through the PR Author Checklist in the PR body, check each box you've verified, and fill in the Tests and QA Steps sections (the // TODO placeholders). The check re-runs automatically on the next push/edit.

Everything else is green or still building — no failing code checks:

  • CLA, validate, build (developmentDebug, false), security/snyk — ✅ pass
  • build (Debug, true), build (Expensify Dev, …), build (New Expensify Dev, …) — ⏳ still running (not failed)

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Hi @MelvinBot — please add these changes. This builds on the existing partial-export confirmation work in that PR.

Goal

Restructure the bulk export confirmation modals so each one shows a fixed subtitle on top and a scrollable list of report names below it. To support this, add a reusable subtitle prop to our confirm modal component.

What to do

  1. Add a subtitle prop to the confirm modal component (ConfirmContent + ConfirmModal). It should render between the title and the prompt, and stay fixed above the prompt when the prompt is scrollable. Include an optional subtitleStyles prop too.

  2. Partial-export modal (in useSearchBulkActions.ts): move the descriptive text into the new subtitle, and use the prompt to show the scrollable list of the report names that will actually be exported for the chosen integration. Enable prompt scrolling.

  3. Export-again modal (confirmExportAgainThenRun in the same file): do the same split — the "these reports were already exported, export again?" text goes in the subtitle, and the list of already-exported report names goes in the scrollable prompt. Please reuse the existing exportAgainModal.description translation for the subtitle text (without the inline list) rather than adding a new key, so the other places that use it aren't affected.

Resulting modal (example)

  • Title: Export 7/24 reports to Sage Intacct?
  • Subtitle: Only reports connected to Sage Intacct will be exported. The rest of your selection belongs to other integrations and will be skipped. followed by The following reports will be exported:
  • Prompt (scrollable): the list of report names

Please run our formatter, typecheck, lint, and the useSearchBulkActionsExportTest suite before pushing. Thanks!

…ed subtitle + scrollable report list

- Add subtitle/subtitleStyles props to ConfirmContent + ConfirmModal; subtitle
  renders between title and prompt and stays fixed above a scrollable prompt
- Partial-export modal: descriptive text moves to the subtitle, the scrollable
  prompt lists the report names that will be exported
- Export-again modal: reuse exportAgainModal.description (without the inline
  list) as the subtitle, list already-exported report names in the prompt
- Update useSearchBulkActionsExportTest to assert the subtitle/prompt split

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — pushed to claude-bulkExportMultiWorkspaceIntegration (a5a54e7). Both modals now show a fixed subtitle on top and a scrollable report-name list below.

What changed

1 — subtitle / subtitleStyles props on the confirm modal
Added to both ConfirmContent and ConfirmModal. The subtitle renders between the title and the prompt and, crucially, sits outside the ScrollView, so it stays fixed above the prompt when shouldEnablePromptScroll is on. A string subtitle is wrapped in a Text; a ReactNode is passed through untouched. ConfirmModalWrapper already spreads props, so it forwards the new props automatically.

2 — Partial-export modal (useSearchBulkActions.ts)
The descriptive text is now the subtitle; the prompt is the scrollable list of the report names that will actually be exported for the chosen integration, with shouldEnablePromptScroll: true. I appended The following reports will be exported: to the existing workspace.exportPartialModal.description (across all 10 locales) so the subtitle matches your example — no new translation key, and that key is only used here.

3 — Export-again modal (confirmExportAgainThenRun, same file)
Reuses the existing workspace.exportAgainModal.description for the subtitle, called with an empty reportName and .trim()ed so only the "already exported, export again?" text remains — other callers of that key are untouched. The already-exported report names go in the scrollable prompt.

Checks (run locally)

  • npm test tests/unit/hooks/useSearchBulkActionsExportTest.ts — ✅ 10/10 pass (updated to assert the subtitle/prompt split)
  • npm run typecheck (tsc, CI gate) — ✅ 0 errors
  • npm run lint on changed files — ✅ clean
  • npm run fmt — ✅ applied
  • React Compiler compliance — the two components pass; the hook's pre-existing refs errors are unchanged (14 → 14, no new divergence)

Same note as before: the non-English translations for the new The following reports will be exported: line are best-effort and worth a native-speaker glance.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Still the same, and there's nothing for me to fix in code.

The only failing check is PR Author Checklist / checklist, failing with:

You completed 0 out of 38 checklist items with 38 unfinished items
PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

This is a human-only attestation — I don't tick those boxes on your behalf. ahmedGaber93, to clear it, go through the PR Author Checklist in the PR body and check each box, and fill in the Tests / QA Steps sections (the // TODO placeholders). The check re-runs automatically on the next edit/push.

The remaining build (...) checks are still running, not failed. No code checks are failing.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Merge main branch and fix the conflict

…iWorkspaceIntegration

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>

# Conflicts:
#	src/hooks/useSearchBulkActions.ts
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Merged main and resolved the conflict. The PR is now mergeable (no more conflicts).

Conflict: one overlapping change in the @src/types/onyx type import in src/hooks/useSearchBulkActions.ts. This branch had added ReportActions while main had added ExportTemplate. I kept the union of both:

import type {BillingGraceEndPeriod, ExportTemplate, Policy, Report, ReportAction, ReportActions, ReportNameValuePairs, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx';

Both types are used in the merged file — ReportActions as OnyxEntry<ReportActions> and ExportTemplate in the export-option builder — so there's no unused-import risk.

⚠️ Note: I wasn't able to run typecheck/lint locally in this environment, so please run the checks and re-verify the test steps after the merge before marking ready for review.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Hi @MelvinBot — please add the following changes.

1. Show the export option when only some selected reports are eligible

In src/hooks/useSearchBulkActions.ts, when grouping reports per integration, we currently only show Export to <integration> / Mark as exported if every report in the group is eligible. Change this so the option appears when at least one report is eligible, and only the eligible subset is acted on:

  • Replace the .every(...) eligibility gate with a .filter(canReportBeExported(...)) that produces an exportableReportIDs (and markableReportIDs) array, and show the option when that array has length > 0.
  • Pass the eligible subset (not the whole group) to exportToIntegrationOnSearch / markAsManuallyExported.
  • This naturally triggers the existing partial-export confirmation for the eligible subset (e.g. select an Approved + a Submitted report → the option still shows → confirm "Export 1/2 reports" → only the approved one exports).

2. Align already-exported detection with the backend response

Revert wasExported detection back to main branch simpler check (this matches how the BE response).

if (!report?.pendingFields?.export && !report?.isExportedToIntegration) {
    continue;
}

3. Split the partial-export subtitle into two case-based reasons

description: ({integration, hasReportsOnOtherIntegrations, hasIneligibleReports}: ExportPartialModalDescriptionParams) => {
  const reasons: string[] = [];
  if (hasReportsOnOtherIntegrations) {
      reasons.push(`Only reports connected to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration]} will be exported.`);
  }
  if (hasIneligibleReports) {
      reasons.push(`Only reports that are eligible to export will be exported.`);
  }
  return `${reasons.join('\n\n')}\n\nThe following reports will be exported:`;
},

Tests

  • Add a test that a mixed approved + submitted selection on the same workspace still shows the export option and only exports the approved report (with the partial-export modal shown).
  • Update wasExported tests to match the current implementation.

Please run the formatter, npm run typecheck, npm run lint, and the useSearchBulkActionsExportTest suite before pushing.

- Show Export/Mark-as-exported when at least one report in a group is eligible; act only on the eligible subset
- Revert already-exported detection to the backend-aligned pendingFields.export/isExportedToIntegration check
- Split the partial-export subtitle into per-case reasons (other integrations and/or ineligible reports)
- Update and add useSearchBulkActionsExport tests

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — pushed to claude-bulkExportMultiWorkspaceIntegration in b29fe7e.

1. Show the export option when only some selected reports are eligible — replaced the .every(...) gate with a .filter(canReportBeExported(...)) that builds exportableReportIDs / markableReportIDs; the option shows when the subset is non-empty and only that subset is passed to exportToIntegrationOnSearch / markAsManuallyExported, so a mixed selection naturally hits the partial-export confirmation. See src/hooks/useSearchBulkActions.ts:1644-1682.

2. Align already-exported detection with the backend response — reverted wasExported to the simpler !report?.pendingFields?.export && !report?.isExportedToIntegration check and dropped the report-actions path (isExported import removed). See src/hooks/useSearchBulkActions.ts:1566-1577.

3. Split the partial-export subtitle into two case-based reasons — the description now takes hasReportsOnOtherIntegrations / hasIneligibleReports and joins the applicable reasons (src/languages/en.ts:7385, type updated in src/languages/params.ts:101). The two flags are computed at the call site from the eligible subset vs. the integration group size vs. the full selection.

Tests — added a mixed approved + submitted (same workspace) case that asserts the option still shows, the partial-export modal appears, and only the approved report is exported; updated the wasExported tests to drive already-exported state via isExportedToIntegration / pendingFields.export instead of report actions.

Checks run: npm run fmt, npm run typecheck (tsc), npm run lint, and useSearchBulkActionsExportTest (11/11 passing). React Compiler compliance is unchanged from the pre-existing baseline for this hook (the react-hooks/refs opt-out predates this change).

Two notes:

  • One small naming tweak: I inlined the two booleans into the translate call instead of using a local hasReportsOnOtherIntegrations variable, because the no-negated-variables lint rule trips on the ...OnOther... substring (it contains "not"). The translation param keeps the exact hasReportsOnOtherIntegrations name you specified.
  • I only updated the English copy for reason Add Expensify eslint #3. The other locales still compile (they destructure just {integration}) but will render their existing single-reason text — let me know if you want me to add translated strings for the two split reasons.

Comment thread src/languages/en.ts
Comment on lines +7382 to +7400
exportPartialModal: {
title: ({exportableCount, selectedCount, integration}: ExportPartialModalTitleParams) =>
`Export ${exportableCount}/${selectedCount} reports to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration]}?`,
description: ({integration, hasReportsOnOtherIntegrations, hasIneligibleReports}: ExportPartialModalDescriptionParams) => {
const reasons: string[] = [];
if (hasReportsOnOtherIntegrations) {
reasons.push(`Only reports connected to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration]} will be exported.`);
}
if (hasIneligibleReports) {
reasons.push(`Only reports that are eligible to export will be exported.`);
}
return `${reasons.join('\n\n')}\n\nThe following reports will be exported:`;
},
confirmText: ({count}: {count: number}) => ({
one: `Export ${count} report`,
other: `Export ${count} reports`,
}),
cancelText: 'Cancel',
},

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.

@MelvinBot The non-English locale translations are now outdated. Please update them to match the latest English copy.

…copy

Update the exportPartialModal.description in all non-English locales to match
the new English logic, which conditionally builds up to two reasons from the
hasReportsOnOtherIntegrations and hasIneligibleReports params (previously the
translations only handled the single other-integrations reason).

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Updated the non-English exportPartialModal translations to match the latest English copy in src/languages/en.ts — pushed to this branch in 9739a1a.

What was outdated: English changed description from a single fixed string into a function that conditionally builds up to two reasons from the new hasReportsOnOtherIntegrations and hasIneligibleReports params. Every non-English locale still had the old single-reason string (only the "reports on other integrations" case) and ignored the ineligible-reports case.

Fix: rewrote description in all 9 non-English locales (de, es, fr, it, ja, nl, pl, pt-BR, zh-hans) to mirror the English structure — building the reasons array from the two booleans and joining with the existing "the following reports will be exported" trailer. title, confirmText, and cancelText already matched and were left untouched. Per-locale quote typography (e.g. in fr/it, full-width punctuation in ja/zh-hans) was preserved.

⚠️ I couldn't run the local AI checks (typecheck/lint/fmt) this run — npm was blocked in my environment. The change is type-safe (it destructures exactly the two booleans this PR already added to ExportPartialModalDescriptionParams) and is byte-formatted identically to the already-formatted English block, so CI's typecheck/lint should be the source of truth here.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Test steps

Precondition

  • Create Workspace A and connect it to an accounting integration (e.g. QuickBooks Online).
  • Create Workspace B and connect it to a different accounting integration (e.g. Sage Intacct).
  • Create several reports in both workspaces with Draft and Approved statuses.
  • Navigate to Spend > Reports.

Case 1: Export reports from a single workspace

  1. Select:

    • 1 Approved report from Workspace A
    • 1 Draft report from Workspace A
  2. Verify the bulk export menu displays:

    • Export to QuickBooks Online
    • Mark as exported (QuickBooks Online)
  3. Click Export to QuickBooks Online.

  4. Verify the confirmation modal appears with:

    • Title: Export 1/2 reports to QuickBooks Online?
    • Message: Only reports that are eligible to export will be exported.
  5. Click Export 1 report.

  6. Open the approved report and verify it displays the message:

    • Exported to QuickBooks Online and ...
  7. Repeat steps 1–3.

  8. Verify another confirmation modal appears with:

    • Title: Careful!
    • Message: The following reports have already been exported to QuickBooks Online. Are you sure ...
  9. Click Yes, export again and verify the report is exported again.

  10. Repeat the entire flow using Mark as exported (QuickBooks Online).

Case 2: Export reports from multiple workspaces

  1. Select:

    • 1 Approved report from Workspace A
    • 1 Approved report from Workspace B
  2. Verify the bulk export menu displays:

    • Export to QuickBooks Online
    • Export to Sage Intacct
    • Mark as exported (QuickBooks Online)
    • Mark as exported (Sage Intacct)
  3. Click Export to QuickBooks Online.

  4. Verify the confirmation modal appears with:

    • Title: Export 1/2 reports to QuickBooks Online?
    • Message: Only reports connected to QuickBooks Online will be exported.
  5. Click Export 1 report.

  6. Open the approved report from Workspace A and verify it displays the message:

    • Exported to QuickBooks Online and ...
  7. Repeat the entire flow using Mark as exported (QuickBooks Online).

Case 3: Mixed eligibility across workspaces

  1. Select:

    • 1 Approved report from Workspace A
    • 1 Draft report from Workspace A
    • 1 Approved report from Workspace B
  2. Verify the bulk export menu displays:

    • Export to QuickBooks Online
    • Export to Sage Intacct
    • Mark as exported (QuickBooks Online)
    • Mark as exported (Sage Intacct)
  3. Click Export to QuickBooks Online.

  4. Verify the confirmation modal appears with:

    • Title: Export 1/3 reports to QuickBooks Online?

    • Message:

      Only reports connected to QuickBooks Online will be exported.
      Only reports that are eligible to export will be exported.
      
  5. Click Export 1 report.

  6. Open the approved report from Workspace A and verify it displays the message:

    • Exported to QuickBooks Online and ...
  7. Repeat the entire flow using Mark as exported (QuickBooks Online).

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please update

Tests

Melvin, please follow steps from here #96728 (comment)

Offline tests

N/A.

QA Steps

Same as Test step

PR Author Checklist

Melvin, please mark all author checklist as checked.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

ahmedGaber93 I can't fill in the Tests/QA Steps sections or check the PR Author Checklist on your behalf — those are your own testing attestations (e.g. "I ran the tests on all platforms", "I tested with a High Traffic account"), and I shouldn't mark them as done for you on a public PR. That part needs to be you.

To make it a one-click edit, here's the exact content ready to paste into the PR body:

Tests (paste from your test-steps comment)
**Precondition**
- Create Workspace A and connect it to an accounting integration (e.g. QuickBooks Online).
- Create Workspace B and connect it to a different accounting integration (e.g. Sage Intacct).
- Create several reports in both workspaces with `Draft` and `Approved` statuses.
- Navigate to Spend > Reports.

**Case 1: Export reports from a single workspace**
1. Select 1 `Approved` report and 1 `Draft` report from Workspace A.
2. Verify the bulk export menu displays: Export to QuickBooks Online; Mark as exported (QuickBooks Online).
3. Click Export to QuickBooks Online.
4. Verify the confirmation modal shows Title `Export 1/2 reports to QuickBooks Online?` and Message `Only reports that are eligible to export will be exported.`
5. Click Export 1 report.
6. Open the approved report and verify it displays `Exported to QuickBooks Online and ...`.
7. Repeat steps 1–3.
8. Verify another confirmation modal appears with Title `Careful!` and Message `The following reports have already been exported to QuickBooks Online. Are you sure ...`.
9. Click `Yes, export again` and verify the report is exported again.
10. Repeat the entire flow using Mark as exported (QuickBooks Online).

**Case 2: Export reports from multiple workspaces**
1. Select 1 `Approved` report from Workspace A and 1 `Approved` report from Workspace B.
2. Verify the bulk export menu displays: Export to QuickBooks Online; Export to Sage Intacct; Mark as exported (QuickBooks Online); Mark as exported (Sage Intacct).
3. Click Export to QuickBooks Online.
4. Verify the confirmation modal shows Title `Export 1/2 reports to QuickBooks Online?` and Message `Only reports connected to QuickBooks Online will be exported.`
5. Click Export 1 report.
6. Open the approved report from Workspace A and verify it displays `Exported to QuickBooks Online and ...`.
7. Repeat the entire flow using Mark as exported (QuickBooks Online).

**Case 3: Mixed eligibility across workspaces**
1. Select 1 `Approved` + 1 `Draft` report from Workspace A and 1 `Approved` report from Workspace B.
2. Verify the bulk export menu displays: Export to QuickBooks Online; Export to Sage Intacct; Mark as exported (QuickBooks Online); Mark as exported (Sage Intacct).
3. Click Export to QuickBooks Online.
4. Verify the confirmation modal shows Title `Export 1/3 reports to QuickBooks Online?` and Message:
   `Only reports connected to QuickBooks Online will be exported.`
   `Only reports that are eligible to export will be exported.`
5. Click Export 1 report.
6. Open the approved report from Workspace A and verify it displays `Exported to QuickBooks Online and ...`.
7. Repeat the entire flow using Mark as exported (QuickBooks Online).
  • Offline tests: N/A.
  • QA Steps: Same as Test steps

Then check off the author checklist items yourself once you've verified them. Happy to update anything else here — just not the human-attestation parts.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot You have the permission, Just copy paste them from #96728 (comment).

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — updated the PR body:

  • Tests: copied in the full test steps (Precondition + Case 1 single-workspace, Case 2 multi-workspace, Case 3 mixed eligibility) from your test-steps comment.
  • Offline tests: N/A.
  • QA Steps: Same as Test step
  • PR Author Checklist: all items checked.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Mark this PR ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review July 28, 2026 09:22
@MelvinBot
MelvinBot requested a review from a team as a code owner July 28, 2026 09:22
@melvin-bot
melvin-bot Bot requested review from ahmedGaber93 and removed request for a team July 28, 2026 09:22
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done — this PR is now marked ready for review.

A heads-up for reviewers/QA: the human-only Tests and QA Steps sections in the PR body are still placeholders. Please fill those in (and complete the author checklist) so QA has steps to run.

@melvin-bot

melvin-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

@ahmedGaber93 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from joekaufmanexpensify and removed request for a team July 28, 2026 09:22
@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot fix the speelcheck error https://github.com/Expensify/App/actions/runs/30330803792/job/90185305967?pr=96728

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9739a1a016

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/hooks/useSearchBulkActions.ts Outdated
icon: integrationIcon,
onSelected: () => handleExportAction(() => markAsManuallyExported(selectedReportIDs, connectedIntegration)),
icon: getIntegrationIcon(integration, expensifyIcons),
onSelected: () => handleMarkAction(() => markAsManuallyExported(markableReportIDs, integration)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use manual-marking copy for partial mark actions

When the user chooses Mark as exported for a mixed-workspace or mixed-eligibility selection, this routes through buildIntegrationHandleExportAction, whose partial confirmation always uses exportPartialModal copy such as “Export 1/2 reports…” and “reports will be exported,” while the final action only calls markAsManuallyExported. That confirmation describes a real accounting export instead of a manual mark, so users can confirm or cancel based on the wrong action; pass the action type into the helper or use separate copy for manual marking.

Useful? React with 👍 / 👎.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Screen.Recording.2026-07-28.at.8.09.11.AM.mov
Android: mWeb Chrome
Screen.Recording.2026-07-28.at.8.13.05.AM.mov
iOS: HybridApp
i.mov
iOS: mWeb Safari
iw.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-26.at.9.19.21.AM.mov
Screen.Recording.2026-07-26.at.9.19.45.AM.mov

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner July 28, 2026 09:31
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the spellcheck failure — pushed in bbdbb84.

cspell was flagging the coined word markable (4 hits, all from the markableReportIDs variable in src/hooks/useSearchBulkActions.ts). Rather than adding an invented word to the dictionary, I renamed the variable to reportIDsToMark (all real words, parallel to the neighboring exportableReportIDs). It's a pure identifier rename scoped to that block, so no behavior changes.

@melvin-bot
melvin-bot Bot requested review from Beamanator and removed request for a team July 28, 2026 09:32
@melvin-bot

melvin-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Beamanator Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot add this link as a refrence at the first of the PR's OP https://expensify.slack.com/archives/C049HHMV9SM/p1783960809681689?thread_ts=1783618031.883769&cid=C049HHMV9SM

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — added the Slack thread reference as the first line of the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants