You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-12] [Bug] Mark as exported processes only the first page after Reports “Select all matching” #101106
Precondition: use a workspace with more than 50 reports that are eligible for Mark as exported / Mark as manually exported.
Go to Spend > Reports.
Filter the list so that it returns more than 50 matching reports (for example, 147).
Click Bulk select and choose Select all so the UI indicates that all matching reports are selected.
Choose Export > Mark as exported (or Mark as manually exported) and confirm the action.
Refresh the list or filter for reports that remain unexported.
Expected Result
The bulk action should apply to every report represented by the cross-page Select all matching selection. If the action is intentionally processed in batches, the UI should clearly show its progress and completion.
Actual Result
The UI can show that all matching reports are selected (for example, 147), but the Reports-tab export action sends only the report IDs on the currently loaded page (about 50). The remaining selected reports are not marked as exported, and the UI provides no indication that the action was limited to a page-sized subset.
Notes
This is specific to the Reports tab's cross-page Select all matching flow. The Expenses tab already sends the query for all matching items; the Reports tab should use the equivalent query-based export path.
selectAllMatchingItems(true) only flips the areAllMatchingItemsSelected boolean; the reconcile effect then marks every loaded row selected (SearchWriteActionsProvider.tsx:152-256). With RESULTS_PAGE_SIZE: 50, that is the ~50-report payload while the header reports 147.
The asymmetry the issue describes is real and sits in the same submenu: the template/CSV export paths drop the ID lists and send the serialized query under all-matching (useSearchBulkActions.ts:869-883), while Mark as exported and Export-to-integration never do.
This cannot be fixed in App alone.MarkAsExportedParams.ts has no query field, and its data payload requires a client-minted optimisticReportActionIDper report — structurally impossible for reports the client has never loaded. Same for ReportExport. A query-based path needs a new backend command.
What changes do you think we should make in order to solve the problem?
Backend (outside App — needs an internal engineer): add a queued, query-based command modelled exactly on QueueBulkPayReports, which already solves this problem for Pay. See its doc comment — "the backend pages through all matches itself" — at src/libs/actions/Search.ts:2037-2043 and QueueBulkPayReportsParams.ts:5. It takes jsonQuery and nothing else.
App: in the Reports-tab export branch, when areAllMatchingItemsSelected is true, call the new command with serializeQueryJSONForBackend(queryJSON) instead of markAsManuallyExported(reportIDsToMark, …), and skip the per-report optimistic Onyx data. This mirrors the Pay branch at useSearchBulkActions.ts:1358-1364.
Interim safeguard, shippable in App today: the silent truncation is the worst part. Until the command exists, either disable Mark as exported under all-matching, or state the real count in the confirmation copy so the user knows only the loaded reports will be marked.
Do not ship a client-only "send the query anyway" change — MarkAsExported would reject or ignore it.
Investigation details
Secondary blockers a query-based Reports export must also handle — each reads loaded data only and has no server-side equivalent today:
Eligibility filtering via canReportBeExported (useSearchBulkActions.ts:1868-1883) and integration grouping (:1897-1913) — the menu is built per-integration from the loaded snapshot, so unloaded reports on a different connection are invisible to the grouping.
The different-companies guard (:1951-1968) and the already-exported warning (:1970-1994).
totalSelectedReportsCount = selectedReportIDs.length (:1917) — the source of the misleading count.
No exclusion support on Reports.shouldPreserveAllMatchingSelection is type === DATA_TYPES.EXPENSE (SearchWriteActionsProvider.tsx:452), so on the Reports tab un-checking a row drops the all-matching flag entirely. A naive query-only fix would need a report-level analogue of getAllMatchingExportQueryAndExclusions (:256-290) or it would mark reports the user explicitly deselected.
Worth checking as part of this — Reports-tab CSV export may have the same bug. In all-matching mode it sends a real jsonQueryand the loaded reportIDList/transactionIDList, because the empty-list treatment and excludedTransactionIDList are both isExpenseType-gated (:1003-1016). Whether the backend prefers the query or the IDs there decides if Reports-tab CSV export is also silently page-limited. I could not determine this from the App repo.
Alternative explanations ruled out: the count is not a display bug — selection genuinely cannot contain unloaded reports, since deriveSelectedReports has no query input. The backend is not silently paging — MarkAsExported receives an explicit data array with no query to page from.
Not verified: backend behaviour of QueueExportSearchToCSV when both jsonQuery and a non-empty reportIDList are sent (requires reading the internal command handler). I did not attempt a live repro — it needs a workspace with 50+ exportable reports on a connected accounting integration.
Existing tests to extend:tests/unit/hooks/useSearchBulkActionsExportTest.ts already mocks the flag via mockAreAllMatchingItemsSelected and has Mark-as-exported cases; tests/unit/hooks/useSearchBulkActionsPayTest.ts:191 shows the shape for the query-based assertion.
Next Steps for Contributor+ team: To accept:@MelvinBot implement this — note I can only build step 3 (the interim safeguard) in App. Steps 1 and 2 need the backend command first, so this needs an internal engineer with access to that repo. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
thanks! quick plan from my side, since the App can't fully fix this on its own:
MarkAsExported takes a per-report data array (reportID + a client-minted optimisticReportActionID), so reports that were never loaded can't be marked from the client. the clean fix mirrors Pay: a queued, query-based backend command like QueueBulkPayReports that takes jsonQuery and pages through all matches on the server.
so the split is:
backend: new query-based command. @Beamanator can you own this one or point me to who does?
App: under select-all matching, call that command with the serialized query instead of the loaded ID list, for both Mark as exported and Export to integration
i'll also raise a PR now for the interim safeguard so we stop silently marking only the loaded page while the backend command is in flight
🤖 Posted by Codex, an AI coding agent acting for @Beamanator.
@dilshodmackbook-sketch Thanks for the plan. I agree with the key observation that MarkAsExported cannot accept jsonQuery directly: it requires explicit report IDs and one optimistic action ID per report.
However, that does not by itself establish that a new backend command is required. App already has a paginated Search path that retrieves the current serialized query at successive offsets. Once all matching pages are hydrated, the existing all-matching reconciliation selects the loaded reports, and the existing markAsManuallyExported / exportToIntegrationOnSearch functions create the required optimistic IDs from their report-ID lists. The observed 50-report outcome is caused by constructing the list before pages 2+ have been loaded, not by a visible 50-report limit in either command App-side implementation.
Before making backend work a blocker, I think we should evaluate an App-only flow:
Under Reports “Select all matching”, sequentially hydrate every page for the current query before enabling or executing report-level export actions.
Reuse the existing eligibility checks, integration grouping, confirmations, and action functions against the fully hydrated selection.
Add a 147-report / 3-page regression test proving MarkAsExported receives 147 unique entries, plus integration-export coverage that preserves the existing grouping behavior.
A queued query-based backend command may still be the right long-term design for very large selections, background progress, or if there is a real server payload/processing limit. But its query-only shape also needs to account for connection-specific grouping, eligibility, and company validation, so it is not simply a QueueBulkPayReports clone.
@MelvinBot Please independently validate this: specifically, determine whether an existing server-side request-size/processing limit or an App pagination constraint prevents the hydrate-then-reuse-existing-commands approach from correctly handling the 147-report case. Please add your analysis here.
You're right on the mechanism, and I was wrong to call a backend command a hard blocker. There is no 50-report limit visible in either command, no client-side request-size cap, no chunking, and no 413 handling anywhere in App — both MarkAsExported and Report_Export already send the entire list in one request. So request size is not what stops this.
But hydrate-then-reuse is blocked today by three App constraints, and there is public evidence of a server-side processing constraint of exactly this shape.
1. Server side: it's processing, not payload size
I can't read the backend from this run, so treat this as directional and confirm internally. The recorded precedent is in the Pay PR that produced QueueBulkPayReports — luacmartins on Expensify/App#94075:
we just removed the capability from the server looping over these reports since it was making the command go to the blocking commit, so I suspect a "pay all" would also lead to stability issues
The agreed replacement was a queued fan-out — one job per report, spaced 1–2s — explicitly so "if 1 report out of 100 fails, it doesn't fail the whole batch."
That constraint is about N reports processed inside one command, and it is indifferent to whether the N arrive as a query or as 147 explicit IDs. Hydrating just moves the loop. It also means a single 147-report MarkAsExported is all-or-nothing: one ineligible report can sink the batch, and App's failureData can't represent a partial result. Open question for an internal engineer: does MarkAsExported / Report_Export have a per-request report cap or blocking-commit exposure? That's the one thing that decides this.
2. App pagination: hydration is possible, but step 1 of your plan isn't sufficient
Page-2+ responses do append into snapshot_<hash>, search() is callable with any offset, there's no offset/page/row cap, and search.hasMoreResults + search.reportCount give a stop condition. Three things still break it:
offset: 0 replaces the snapshot, per App's own comment at index.tsx:837-838. Several non-pagination callers fire offset 0 on the current query — including refreshSearchAfterReportAction, which the export flow itself calls. A hydration loop is racy against the very flow it serves.
Hydration alone doesn't grow the selection.deriveSelectedReports walks filteredData — rendered rows. Hydrated-but-unrendered rows need the derivation changed too, not just the fetch.
Policy data is read from live Onyx, not the snapshot.canReportBeExported, the integration grouping, and the different-companies guard all read policies from useOnyx(COLLECTION.POLICY) with no snapshot fallback — unlike the pay/approve paths, which use getPolicyFromSearchSnapshot. Any hydrated report whose policy isn't in live Onyx is silently filtered out of the export, which is the same class of quiet undercount as the original bug. This is a latent bug hydration would amplify, and it should be fixed either way.
3. Client-side processing: App has already shipped this exact deploy blocker
The "act on every selected item client-side" pattern froze the page at 200+ items in #87604 (duplicate). The resolution was a hard cap, not chunking: CONST.SEARCH.BULK_DUPLICATE_LIMIT = 50 with user-facing copy in every locale. markAsManuallyExported at 147 reports is 294 persisted-key merges in a single Onyx.update() plus ~735 OnyxUpdate entries serialized into the persisted request. Probably survivable at 147; the 10k-report query has no client cap to stop it.
Where that leaves the decision
Your plan is viable for the 147 case if internal confirms no per-request cap, but it is not "reuse the existing functions" — it needs the offset-0 race handled, deriveSelectedReports extended beyond rendered rows, the policy source switched to the snapshot, and a bound on total reports. I'd also note App's select-all menu deliberately only exposes the two actions that have server-side query paths (useSearchBulkActions.ts:2215-2216) — Mark as exported is a leak in that design because it sits inside the Export submenu.
Either way, the interim safeguard stands on its own and is worth shipping now: stop silently marking only the loaded page.
Supporting detail
Payload shapes (verified).MarkAsExportedParams is {markedManually, data} where data is a stringified array of {reportID, label, optimisticReportActionID}. ReportExportParams is {reportIDList, connectionName, type, optimisticReportActions}. Neither has a query field. The CSV/template params do carry jsonQuery, which is why the Expenses tab already covers all-matching.
Network layer. Payloads are plain multipart FormData POSTs with no length inspection (prepareRequestPayload), and CONST.HTTP_STATUS has no 413. If the edge ever rejected an oversized write, App would fall into the generic statusText branch with no specific messaging.
Selection does survive new pages. The reconcile effect commits without totalSelectableItemsCount, so areAllMatchingItemsSelected is not downgraded when a page lands. Selection is cleared on query-hash change, groupBy change, empty results, and blur — not on an ordinary snapshot update. So a hydration loop wouldn't lose the flag.
The "147" is server-provided.SearchBulkActionsButton.tsx:179 reads search.reportCount, with an inline comment that the local selection "only holds the loaded page, so it can't be derived locally." That's also the completion check a hydrator would use.
Separate rollback bug worth fixing regardless.markAsManuallyExported rolls back with isExportedToIntegration: previousExportedValue, read from live Onyx. For a snapshot-only report that value is undefined, and Onyx's merge skips undefined source properties — so on server failure the local isExportedToIntegration: true sticks. At 147 reports that's a large silent wrong-state surface. I verified the merge behavior in react-native-onyx; worth a quick confirmation before acting on it.
Test coverage gap.tests/unit/hooks/useSearchBulkActionsExportTest.ts mocks both action modules wholesale, so it asserts only ID lists — never payload shape, counts, or optimistic data. mockAreAllMatchingItemsSelected is set true in five tests, none of them with the Reports-tab query, and no test uses more than two reports. Your 147-report / 3-page regression test would be genuinely new coverage, and it should assert the payload from a hydrated snapshot rather than a hand-fed selectedReports.
Not verified (no access to the internal repos in this run): whether MarkAsExported / Report_Export have a per-request report cap, timeout, or blocking-commit exposure; whether the Search response's policy_ rows carry employeeList and connections.*.config.credentials; and whether QueueExportSearchToCSV prefers the query or the IDs when both are sent.
Also relevant: the Move/Add-to-report select-all fix took the same query-based route rather than hydration (#98411, after #96482 was reverted), citing all-or-nothing batch semantics — "one expense the backend can't move rejects the entire move."
Next Steps for Contributor+ team: To accept:@MelvinBot implement this — I can build the interim safeguard and the policy-source fix in App today. The hydration loop should wait on an answer to the per-request-cap question. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting this analysis.
changed the title [-][Bug] Mark as exported processes only the first page after Reports “Select all matching”[/-][+][Due for payment 2026-10-12] [Bug] Mark as exported processes only the first page after Reports “Select all matching”[/+]on Oct 5, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.2-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-12. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
Activity
Beamanator commentedon Sep 14, 2026
@dilshodmackbook-sketch is working on this 🙏
David-2002065 commentedon Sep 14, 2026
Thanks a lot, can you assign me here
MelvinBot commentedon Sep 14, 2026
Proposal
What is the root cause of that problem?
The Reports tab's Mark as exported builds its payload from the rows currently rendered, not from the search query.
reportIDsToMarkis derived fromselectedReportsand passed as a literal ID list tomarkAsManuallyExported, with no query involved:src/hooks/useSearchBulkActions.ts:2089-2102—reportIDsToMark→markAsManuallyExported(reportIDsToMark, …)src/components/Search/selectionBuilders.ts:246-249—deriveSelectedReportsonly loops overdata, the loaded rows. A report not indatacan never be selected.selectAllMatchingItems(true)only flips theareAllMatchingItemsSelectedboolean; the reconcile effect then marks every loaded row selected (SearchWriteActionsProvider.tsx:152-256). WithRESULTS_PAGE_SIZE: 50, that is the ~50-report payload while the header reports 147.The asymmetry the issue describes is real and sits in the same submenu: the template/CSV export paths drop the ID lists and send the serialized query under all-matching (
useSearchBulkActions.ts:869-883), while Mark as exported and Export-to-integration never do.This cannot be fixed in App alone.
MarkAsExportedParams.tshas no query field, and itsdatapayload requires a client-mintedoptimisticReportActionIDper report — structurally impossible for reports the client has never loaded. Same forReportExport. A query-based path needs a new backend command.What changes do you think we should make in order to solve the problem?
Backend (outside App — needs an internal engineer): add a queued, query-based command modelled exactly on
QueueBulkPayReports, which already solves this problem for Pay. See its doc comment — "the backend pages through all matches itself" — atsrc/libs/actions/Search.ts:2037-2043andQueueBulkPayReportsParams.ts:5. It takesjsonQueryand nothing else.App: in the Reports-tab export branch, when
areAllMatchingItemsSelectedis true, call the new command withserializeQueryJSONForBackend(queryJSON)instead ofmarkAsManuallyExported(reportIDsToMark, …), and skip the per-report optimistic Onyx data. This mirrors the Pay branch atuseSearchBulkActions.ts:1358-1364.Interim safeguard, shippable in App today: the silent truncation is the worst part. Until the command exists, either disable Mark as exported under all-matching, or state the real count in the confirmation copy so the user knows only the loaded reports will be marked.
Do not ship a client-only "send the query anyway" change —
MarkAsExportedwould reject or ignore it.Investigation details
Secondary blockers a query-based Reports export must also handle — each reads loaded data only and has no server-side equivalent today:
canReportBeExported(useSearchBulkActions.ts:1868-1883) and integration grouping (:1897-1913) — the menu is built per-integration from the loaded snapshot, so unloaded reports on a different connection are invisible to the grouping.:1951-1968) and the already-exported warning (:1970-1994).totalSelectedReportsCount = selectedReportIDs.length(:1917) — the source of the misleading count.shouldPreserveAllMatchingSelectionistype === DATA_TYPES.EXPENSE(SearchWriteActionsProvider.tsx:452), so on the Reports tab un-checking a row drops the all-matching flag entirely. A naive query-only fix would need a report-level analogue ofgetAllMatchingExportQueryAndExclusions(:256-290) or it would mark reports the user explicitly deselected.Worth checking as part of this — Reports-tab CSV export may have the same bug. In all-matching mode it sends a real
jsonQueryand the loadedreportIDList/transactionIDList, because the empty-list treatment andexcludedTransactionIDListare bothisExpenseType-gated (:1003-1016). Whether the backend prefers the query or the IDs there decides if Reports-tab CSV export is also silently page-limited. I could not determine this from the App repo.Alternative explanations ruled out: the count is not a display bug — selection genuinely cannot contain unloaded reports, since
deriveSelectedReportshas no query input. The backend is not silently paging —MarkAsExportedreceives an explicitdataarray with no query to page from.Not verified: backend behaviour of
QueueExportSearchToCSVwhen bothjsonQueryand a non-emptyreportIDListare sent (requires reading the internal command handler). I did not attempt a live repro — it needs a workspace with 50+ exportable reports on a connected accounting integration.Existing tests to extend:
tests/unit/hooks/useSearchBulkActionsExportTest.tsalready mocks the flag viamockAreAllMatchingItemsSelectedand has Mark-as-exported cases;tests/unit/hooks/useSearchBulkActionsPayTest.ts:191shows the shape for the query-based assertion.Possibly related issues
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement this— note I can only build step 3 (the interim safeguard) in App. Steps 1 and 2 need the backend command first, so this needs an internal engineer with access to that repo.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run · no recording available
melvin-bot commentedon Sep 14, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @mananjadhav (
External)melvin-bot commentedon Sep 14, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
David-2002065 commentedon Sep 14, 2026
thanks! quick plan from my side, since the App can't fully fix this on its own:
MarkAsExported takes a per-report
dataarray (reportID + a client-mintedoptimisticReportActionID), so reports that were never loaded can't be marked from the client. the clean fix mirrors Pay: a queued, query-based backend command likeQueueBulkPayReportsthat takesjsonQueryand pages through all matches on the server.so the split is:
i'll also raise a PR now for the interim safeguard so we stop silently marking only the loaded page while the backend command is in flight
Beamanator commentedon Sep 14, 2026
🤖 Posted by Codex, an AI coding agent acting for @Beamanator.
@dilshodmackbook-sketch Thanks for the plan. I agree with the key observation that MarkAsExported cannot accept jsonQuery directly: it requires explicit report IDs and one optimistic action ID per report.
However, that does not by itself establish that a new backend command is required. App already has a paginated Search path that retrieves the current serialized query at successive offsets. Once all matching pages are hydrated, the existing all-matching reconciliation selects the loaded reports, and the existing markAsManuallyExported / exportToIntegrationOnSearch functions create the required optimistic IDs from their report-ID lists. The observed 50-report outcome is caused by constructing the list before pages 2+ have been loaded, not by a visible 50-report limit in either command App-side implementation.
Before making backend work a blocker, I think we should evaluate an App-only flow:
A queued query-based backend command may still be the right long-term design for very large selections, background progress, or if there is a real server payload/processing limit. But its query-only shape also needs to account for connection-specific grouping, eligibility, and company validation, so it is not simply a QueueBulkPayReports clone.
@MelvinBot Please independently validate this: specifically, determine whether an existing server-side request-size/processing limit or an App pagination constraint prevents the hydrate-then-reuse-existing-commands approach from correctly handling the 147-report case. Please add your analysis here.
MelvinBot commentedon Sep 14, 2026
Validation
You're right on the mechanism, and I was wrong to call a backend command a hard blocker. There is no 50-report limit visible in either command, no client-side request-size cap, no chunking, and no 413 handling anywhere in App — both
MarkAsExportedandReport_Exportalready send the entire list in one request. So request size is not what stops this.But hydrate-then-reuse is blocked today by three App constraints, and there is public evidence of a server-side processing constraint of exactly this shape.
1. Server side: it's processing, not payload size
I can't read the backend from this run, so treat this as directional and confirm internally. The recorded precedent is in the Pay PR that produced
QueueBulkPayReports—luacmartinson Expensify/App#94075:The agreed replacement was a queued fan-out — one job per report, spaced 1–2s — explicitly so "if 1 report out of 100 fails, it doesn't fail the whole batch."
That constraint is about N reports processed inside one command, and it is indifferent to whether the N arrive as a query or as 147 explicit IDs. Hydrating just moves the loop. It also means a single 147-report
MarkAsExportedis all-or-nothing: one ineligible report can sink the batch, and App'sfailureDatacan't represent a partial result. Open question for an internal engineer: doesMarkAsExported/Report_Exporthave a per-request report cap or blocking-commit exposure? That's the one thing that decides this.2. App pagination: hydration is possible, but step 1 of your plan isn't sufficient
Page-2+ responses do append into
snapshot_<hash>,search()is callable with any offset, there's no offset/page/row cap, andsearch.hasMoreResults+search.reportCountgive a stop condition. Three things still break it:offset: 0replaces the snapshot, per App's own comment atindex.tsx:837-838. Several non-pagination callers fire offset 0 on the current query — includingrefreshSearchAfterReportAction, which the export flow itself calls. A hydration loop is racy against the very flow it serves.deriveSelectedReportswalksfilteredData— rendered rows. Hydrated-but-unrendered rows need the derivation changed too, not just the fetch.canReportBeExported, the integration grouping, and the different-companies guard all readpoliciesfromuseOnyx(COLLECTION.POLICY)with no snapshot fallback — unlike the pay/approve paths, which usegetPolicyFromSearchSnapshot. Any hydrated report whose policy isn't in live Onyx is silently filtered out of the export, which is the same class of quiet undercount as the original bug. This is a latent bug hydration would amplify, and it should be fixed either way.3. Client-side processing: App has already shipped this exact deploy blocker
The "act on every selected item client-side" pattern froze the page at 200+ items in #87604 (duplicate). The resolution was a hard cap, not chunking:
CONST.SEARCH.BULK_DUPLICATE_LIMIT = 50with user-facing copy in every locale.markAsManuallyExportedat 147 reports is 294 persisted-key merges in a singleOnyx.update()plus ~735OnyxUpdateentries serialized into the persisted request. Probably survivable at 147; the 10k-report query has no client cap to stop it.Where that leaves the decision
Your plan is viable for the 147 case if internal confirms no per-request cap, but it is not "reuse the existing functions" — it needs the offset-0 race handled,
deriveSelectedReportsextended beyond rendered rows, the policy source switched to the snapshot, and a bound on total reports. I'd also note App's select-all menu deliberately only exposes the two actions that have server-side query paths (useSearchBulkActions.ts:2215-2216) — Mark as exported is a leak in that design because it sits inside the Export submenu.Either way, the interim safeguard stands on its own and is worth shipping now: stop silently marking only the loaded page.
Supporting detail
Payload shapes (verified).
MarkAsExportedParamsis{markedManually, data}wheredatais a stringified array of{reportID, label, optimisticReportActionID}.ReportExportParamsis{reportIDList, connectionName, type, optimisticReportActions}. Neither has a query field. The CSV/template params do carryjsonQuery, which is why the Expenses tab already covers all-matching.Network layer. Payloads are plain multipart FormData POSTs with no length inspection (
prepareRequestPayload), andCONST.HTTP_STATUShas no 413. If the edge ever rejected an oversized write, App would fall into the genericstatusTextbranch with no specific messaging.Selection does survive new pages. The reconcile effect commits without
totalSelectableItemsCount, soareAllMatchingItemsSelectedis not downgraded when a page lands. Selection is cleared on query-hash change, groupBy change, empty results, and blur — not on an ordinary snapshot update. So a hydration loop wouldn't lose the flag.The "147" is server-provided.
SearchBulkActionsButton.tsx:179readssearch.reportCount, with an inline comment that the local selection "only holds the loaded page, so it can't be derived locally." That's also the completion check a hydrator would use.Separate rollback bug worth fixing regardless.
markAsManuallyExportedrolls back withisExportedToIntegration: previousExportedValue, read from live Onyx. For a snapshot-only report that value isundefined, and Onyx's merge skipsundefinedsource properties — so on server failure the localisExportedToIntegration: truesticks. At 147 reports that's a large silent wrong-state surface. I verified the merge behavior inreact-native-onyx; worth a quick confirmation before acting on it.Test coverage gap.
tests/unit/hooks/useSearchBulkActionsExportTest.tsmocks both action modules wholesale, so it asserts only ID lists — never payload shape, counts, or optimistic data.mockAreAllMatchingItemsSelectedis set true in five tests, none of them with the Reports-tab query, and no test uses more than two reports. Your 147-report / 3-page regression test would be genuinely new coverage, and it should assert the payload from a hydrated snapshot rather than a hand-fedselectedReports.Not verified (no access to the internal repos in this run): whether
MarkAsExported/Report_Exporthave a per-request report cap, timeout, or blocking-commit exposure; whether the Search response'spolicy_rows carryemployeeListandconnections.*.config.credentials; and whetherQueueExportSearchToCSVprefers the query or the IDs when both are sent.Also relevant: the Move/Add-to-report select-all fix took the same query-based route rather than hydration (#98411, after #96482 was reverted), citing all-or-nothing batch semantics — "one expense the backend can't move rejects the entire move."
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement this— I can build the interim safeguard and the policy-source fix in App today. The hydration loop should wait on an answer to the per-request-cap question.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting this analysis.
view run · no recording available
36 remaining items
[-][Bug] Mark as exported processes only the first page after Reports “Select all matching”[/-][+][Due for payment 2026-10-12] [Bug] Mark as exported processes only the first page after Reports “Select all matching”[/+]melvin-bot commentedon Oct 5, 2026
@mananjadhav
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.2-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-12. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
melvin-bot commentedon Oct 9, 2026
@mananjadhav Whoops! This issue is 2 days overdue. Let's get this updated quick!
melvin-bot commentedon Oct 11, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)mallenexpensify commentedon Oct 14, 2026
@mananjadhav paid, summary updated above. Thx.
garrettmknight commentedon Oct 19, 2026
$250 approved for @mananjadhav