Skip to content

Fix stale 'from' filter persisting for workspace auditor#82475

Closed
MelvinBot wants to merge 4 commits intomainfrom
claude-fixAuditorFilterPersistence
Closed

Fix stale 'from' filter persisting for workspace auditor#82475
MelvinBot wants to merge 4 commits intomainfrom
claude-fixAuditorFilterPersistence

Conversation

@MelvinBot
Copy link
Copy Markdown
Contributor

Explanation of Change

When a workspace auditor navigates to Reports > Expenses, they land on the "Submit" view which hardcodes a from:[self] filter scoping results to only their own submissions. When they then apply additional filters (date range, category, tag) or open Advanced Filters, the stale from field persists in the Onyx form and carries forward into the rebuilt query string, so they only ever see their own expenses instead of all submitters.

This PR fixes the stale from persistence through two changes:

  1. Use Onyx.set (full replace) instead of Onyx.merge when opening Advanced Filters — In openAdvancedFilters, we now pass true to updateAdvancedFilters so the Onyx form is fully replaced with the current URL-derived filter values. This prevents stale fields like from from surviving when they're no longer part of the active query.

  2. Clear from when the search type changes — When the user switches the search type (e.g., from Submit to Expenses), we now reset from to undefined, consistent with how status and columns are already reset on type change.

Fixed Issues

$ #82462
PROPOSAL: #82462 (comment)

Tests

  1. Log in as a workspace auditor (not an approver)
  2. Navigate to Reports tab — you should land on the "Submit" view
  3. Apply date range filters (before/after for last year) along with category and tag filters
  4. Verify that expenses from all submitters on the workspace are visible, not just the current user's
  5. Click the "Filters" button to open Advanced Filters
  6. Verify the "From" field is empty (not pre-filled with the current user)
  7. Apply filters from the Advanced Filters panel
  8. Verify results include expenses from all submitters matching the filter criteria
  • Verify that no errors appear in the JS console

Offline tests

These changes only affect how the Onyx filter form is synced when applying filters. No network requests are involved in the filter form update logic, so offline behavior is unchanged.

QA Steps

  1. Log in as a workspace auditor (not an approver) on a workspace with multiple submitters
  2. Navigate to Reports > Expenses
  3. Apply date range filters (before/after) and select categories/tags
  4. Verify expenses from all submitters are visible, not just one user
  5. Open Advanced Filters and verify the "From" field is not pre-populated
  6. Apply various filter combinations and verify results are not scoped to a single user
  • 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 verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • 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)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • 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)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • 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

… search filters

Use Onyx.set (full replace) instead of Onyx.merge when opening Advanced
Filters to ensure stale fields like 'from' don't persist. Also clear the
'from' filter when the search type changes, similar to how 'status' and
'columns' are already reset.

Fixes #82462

Co-authored-by: Krishna <Krishna2323@users.noreply.github.com>
@MelvinBot MelvinBot requested a review from a team February 13, 2026 23:36
@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-fixAuditorFilterPersistence
git checkout -b claude-fixAuditorFilterPersistence upstream/claude-fixAuditorFilterPersistence
git push -u origin claude-fixAuditorFilterPersistence

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

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 13, 2026

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 Δ
...earchAdvancedFiltersPage/SearchFiltersTypePage.tsx 0.00% <ø> (ø)
src/types/form/SearchAdvancedFiltersForm.ts 100.00% <100.00%> (ø)
...nents/Search/SearchPageHeader/SearchFiltersBar.tsx 65.38% <33.33%> (-0.43%) ⬇️
... and 343 files with indirect coverage changes

@Julesssss Julesssss marked this pull request as ready for review February 13, 2026 23:45
@Julesssss Julesssss requested a review from a team as a code owner February 13, 2026 23:45
@melvin-bot melvin-bot bot requested review from JmillsExpensify and removed request for a team February 13, 2026 23:45
@Krishna2323
Copy link
Copy Markdown
Contributor

Reviewing....

@Krishna2323
Copy link
Copy Markdown
Contributor

Hey @MelvinBot I have few questions about the expected behaviour and the implementation. The PR seems to be working fine.

Questions about Expected Behavior

  1. When the auditor changes the type from "Expense Report" to "Expense" via the filter bar, should only from be cleared, or should to, action, payer, and exporter also be cleared? These are all view-specific implicit filters — Submit sets from + action:submit, Approve sets to + action:approve, Pay sets payer + action:pay, Export sets exporter + action:export. If we only clear from, the same bug can occur for approvers switching type from the Approve view (stale to:[self] would persist).

  2. If a user explicitly sets a from filter themselves (via Advanced Filters → From page) and then changes the type, should that explicit from also be cleared? The current fix doesn't distinguish between an implicitly injected from (from the Submit view) and a user-intentionally-set from. Is losing the user's explicit filter acceptable on type change?

  3. What should the auditor's default landing view be? Currently they land on Submit (scoped to self). Should auditors instead land on Expenses (unscoped) since their role is to review all workspace expenses? Or is Submit still the correct default?


Questions about Incomplete/Wrong Implementation

  1. Why is SearchFiltersTypePage.tsx not included in the fix? When the user changes type through Advanced Filters → Type page, applyChanges at line 45-56 uses Onyx.merge and does NOT clear from/to/action. The same stale filter persistence can happen through this path. Shouldn't the same clearing logic be added there?

  2. Should to and action also be cleared alongside from in the type-change block? Currently only from is cleared. But the Approve view injects to:[self] + action:approve and the Pay view injects payer:[self] + action:pay. If an approver lands on the Approve view and changes type via the filter bar, to:[self] and action:approve would persist — the exact same class of bug.

  3. Is the Onyx.set change in openAdvancedFilters actually needed given that SearchPage.tsx already does Onyx.set on every query change (line 245-247)? By the time the user clicks "Advanced Filters", the useEffect in SearchPage should have already synced the Onyx form via Onyx.set. What specific scenario does this second change address that the existing sync doesn't already handle?

cc: @Julesssss

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Great questions @Krishna2323. I've investigated the codebase in depth to answer each one. These are genuinely important observations about the completeness of this fix.


Questions about Expected Behavior

1. Should to, action, payer, and exporter also be cleared on type change?

Yes, this is a valid gap. Each view injects a distinct set of implicit filters (SearchUIUtils.ts:599-680):

View Implicit Filters
Submit from:[self] + action:submit
Approve to:[self] + action:approve
Pay payer:[self] + action:pay + reimbursable:yes
Export exporter:[self] + action:export + exportedOn:never

Critically, all of these filter keys (from, to, action, payer, exporter) are present in ALLOWED_TYPE_FILTERS for both the expense and expense_report types (SearchAdvancedFiltersForm.ts:176-329). This means buildQueryStringFromFilterFormValues will NOT strip them via isFilterSupported() when switching types -- they will persist in the generated query string.

So yes, an approver switching from the Approve view would carry stale to:[self] + action:approve. The same class of bug applies to Pay (payer) and Export (exporter). Clearing only from is insufficient -- to, action, payer, exporter, reimbursable, and exportedOn should also be cleared on type change.

2. Should user-explicit from also be cleared on type change?

The current code does not distinguish between implicit (view-injected) and explicit (user-set) from filters -- both are stored the same way in the Onyx form and the URL query string. There's no metadata tracking how a filter was set.

Given this, clearing from on type change seems like a reasonable trade-off. When a user changes the search type, the semantics of the search change substantially (different data model, different result sets). View-specific filters like from scoped to a particular user are likely no longer meaningful in the new type context. Losing a user-explicit from on type change is a minor inconvenience compared to the bug of stale filters producing confusing results.

That said, this is a product decision -- if preserving user-explicit filters across type changes is considered important, an approach like tracking filter provenance (implicit vs explicit) would be needed, which adds complexity.

3. What should the auditor's default landing view be?

Currently, the default is determined by getDefaultActionableSearchMenuItem() at SearchUIUtils.ts:843-845, which prefers Approve, then falls back to Submit.

For an auditor who is NOT an approver/payer/exporter, the visibility logic at SearchUIUtils.ts:873-903 shows:

  • Submit: Visible (because isEligibleForSubmitSuggestion = isPaidPolicy, and auditors are paid policy members)
  • Approve: NOT visible (requires isApprover || isSubmittedTo)
  • Pay: NOT visible (requires isPayer)
  • Export: NOT visible (requires isExporter)

So auditors land on Submit, which is scoped to from:[self] -- showing only their own reports needing submission. This seems suboptimal for an auditor whose role is to review all workspace expenses. However, changing the default landing view is a separate product decision and likely a separate issue from this PR's scope.


Questions about Incomplete/Wrong Implementation

4. Why is SearchFiltersTypePage.tsx not included in the fix?

This is a valid concern. Looking at SearchFiltersTypePage.tsx:45-58:

const applyChanges = useCallback(() => {
    const updatedFilters = {
        type: selectedItem,
        ...(hasTypeChanged && {
            groupBy: null,
            status: CONST.SEARCH.STATUS.EXPENSE.ALL,
            has: filteredHasValues,
        }),
    };
    updateAdvancedFilters(updatedFilters);  // uses Onyx.merge (no `true` second arg)
}, ...);

This path:

  1. Does NOT clear from, to, action, payer, exporter, or any view-specific filters on type change
  2. Uses Onyx.merge (not Onyx.set), so all stale fields in the Onyx form persist

This is the same class of bug through a different code path (Advanced Filters > Type page vs the filter bar dropdown). The same clearing logic should be applied here.

5. Should to and action also be cleared alongside from?

Yes -- as detailed in the answer to question #1 above, only clearing from addresses the Submit view scenario but leaves the same bug for Approve (to + action), Pay (payer + action), and Export (exporter + action). All view-specific implicit filters should be cleared when the type changes.

Specifically, the type-change block at SearchFiltersBar.tsx:312-319 should also clear: to, action, payer, exporter, reimbursable, and exportedOn.

6. Is the Onyx.set change in openAdvancedFilters actually needed?

Looking at SearchPage.tsx:244-247:

const formValues = useFilterFormValues(queryJSON);

useEffect(() => {
    updateAdvancedFilters(formValues, true);  // Onyx.set -- full replace
}, [formValues]);

This useEffect runs on every formValues change (which derives from queryJSON, which derives from the URL q param). So whenever the URL query changes, the Onyx form is fully replaced via Onyx.set.

The openAdvancedFilters change at SearchFiltersBar.tsx:347-350 calls updateAdvancedFilters(filterFormValues, true) right before navigating to the Advanced Filters page. Since filterFormValues is derived from the same queryJSON and SearchPage's useEffect should have already synced the form, this second Onyx.set appears redundant in the normal flow.

A possible scenario where it could matter: if the useEffect in SearchPage hasn't fired yet due to React's batched state updates (e.g., the URL changed but the effect hasn't run before the user taps "Advanced Filters"). However, this would be a very narrow race condition. The PR author (@Julesssss) may have additional context on a specific scenario this addresses.


Summary

The reviewer's observations are well-founded. The key gaps are:

  1. Incomplete filter clearing: Only from is cleared, but to, action, payer, exporter, reimbursable, and exportedOn face the same stale persistence issue
  2. Missing code path: SearchFiltersTypePage.tsx is not patched, leaving the same bug accessible through Advanced Filters > Type
  3. Potentially redundant change: The Onyx.set change in openAdvancedFilters may be redundant with SearchPage's existing useEffect sync

These seem like issues that should be addressed before merging. cc @Julesssss

@Julesssss
Copy link
Copy Markdown
Contributor

@MelvinBot so what do you think about the feedback? Can we solve this without the wider refactor? Recall your original proposal here: #82462 (comment)

@MelvinBot
Copy link
Copy Markdown
Contributor Author

@Julesssss Yes, I think the feedback can be addressed without a wider refactor. Krishna's points are valid but they're all extensions of the same pattern already in this PR — not a different architecture. Here's my take:

What can be solved with small additions to this PR

Points #1 and #5 (clear all view-specific filters, not just from):
This is the most important feedback item. The fix is a straightforward extension of the existing type-change block in updateFilterForm — just clear more fields:

if (updatedFilterFormValues.type !== searchAdvancedFiltersForm.type) {
    updatedFilterFormValues.columns = [];
    updatedFilterFormValues.status = CONST.SEARCH.STATUS.EXPENSE.ALL;
    updatedFilterFormValues.from = undefined;
    updatedFilterFormValues.to = undefined;        // Approve view
    updatedFilterFormValues.action = undefined;     // All views set action
    updatedFilterFormValues.payer = undefined;       // Pay view
    updatedFilterFormValues.exporter = undefined;    // Export view
    updatedFilterFormValues.reimbursable = undefined; // Pay view
    updatedFilterFormValues.exportedOn = undefined;  // Export view
    updatedFilterFormValues.has = filterValidHasValues(...);
}

This is the same approach — no new abstractions or refactoring needed.

Point #4 (SearchFiltersTypePage.tsx missing):
The same clearing logic should be added to SearchFiltersTypePage.tsx:48-55. Currently when type changes there, only groupBy, status, and has are cleared. Add the same view-specific filter fields (from, to, action, payer, exporter, reimbursable, exportedOn) set to undefined:

const updatedFilters = {
    type: selectedItem,
    ...(hasTypeChanged && {
        groupBy: null,
        status: CONST.SEARCH.STATUS.EXPENSE.ALL,
        has: filteredHasValues,
        from: undefined,
        to: undefined,
        action: undefined,
        payer: undefined,
        exporter: undefined,
        reimbursable: undefined,
        exportedOn: undefined,
    }),
};

Note: Since SearchFiltersTypePage uses Onyx.merge (no true flag), setting these to undefined via Onyx.merge should clear them in Onyx (merge treats undefined values as deletions in nested objects). Alternatively, pass null which is explicitly handled as a deletion by Onyx.

What can be deferred (out of scope for this issue)

Point #2 (implicit vs explicit filter distinction): The original proposal acknowledged this trade-off. Clearing all view-specific filters on type change is the right behavior regardless — when you switch from "Expense Report" to "Expense", the action:submit/action:approve filters are semantically meaningless in the new context. Distinguishing implicit vs explicit would require tracking filter provenance, which is a larger design change that's not needed to fix this bug.

Point #3 (auditor default landing view): This is a separate product question about whether auditors should default to Expenses instead of Submit. It's not related to the stale filter persistence bug. Could be filed as a separate issue.

Point #6 (Onyx.set in openAdvancedFilters): I agree this is likely redundant given SearchPage's useEffect. However, it's a defensive safeguard against the async race condition described in the original proposal. The cost of keeping it is negligible (one extra Onyx.set call that's a no-op if the form is already in sync), while removing it reintroduces the theoretical race. I'd keep it.

Summary

The changes needed to address the feedback are:

  1. Expand the cleared fields in SearchFiltersBar.tsx's type-change block (add to, action, payer, exporter, reimbursable, exportedOn) — ~6 additional lines
  2. Add the same clearing logic to SearchFiltersTypePage.tsx's applyChanges — ~7 additional lines
  3. Keep the Onyx.set change as-is (defensive, low cost)

All three are small, additive changes within the same pattern. No wider refactor needed.

@Krishna2323
Copy link
Copy Markdown
Contributor

@MelvinBot could you please implement the changes you mentioned in your last comment?

When switching search types, stale view-specific filters (to, action, payer,
exporter, reimbursable, exportedOn) were persisting alongside the already-cleared
'from' filter. This caused the same class of bug for Approve, Pay, and Export
views that was fixed for Submit.

Also adds the same clearing logic to SearchFiltersTypePage.tsx, which handles
type changes through the Advanced Filters > Type page path.

Co-authored-by: Krishna <Krishna2323@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

@Krishna2323 I've pushed a commit addressing the gaps you identified. Here's what changed:

Changes made

1. SearchFiltersBar.tsx — Clear all view-specific implicit filters on type change (questions #1 and #5)

The type-change block now clears to, action, payer, exporter, reimbursable, and exportedOn alongside the already-cleared from. This prevents stale filters from any view (Submit, Approve, Pay, Export) from persisting when the user switches types via the filter bar.

2. SearchFiltersTypePage.tsx — Add the same clearing logic (question #4)

The applyChanges callback in the Advanced Filters > Type page now also clears the same set of view-specific filters when the type changes. Previously this path only reset groupBy, status, and has.

3. openAdvancedFilters Onyx.set change (question #6) — kept as-is

I left the existing Onyx.set change in openAdvancedFilters in place. While SearchPage's useEffect should handle most cases, this serves as a defensive sync before navigating to the Advanced Filters page. It doesn't cause harm and may protect against edge-case timing issues with React's batched state updates.

Not addressed in this commit

Question #3 (auditor default landing view) — This is a separate product decision about whether auditors should land on Expenses (unscoped) instead of Submit (scoped to self). The current behavior is driven by getDefaultActionableSearchMenuItem() in SearchUIUtils.ts which prefers Approve > Submit for any paid policy member. Changing this would be a separate issue/PR.

cc @Julesssss

Comment on lines +54 to +60
from: undefined,
to: undefined,
action: undefined,
payer: undefined,
exporter: undefined,
reimbursable: undefined,
exportedOn: undefined,
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 this should be null not undefined, else Onyx won't clear them properly

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.

Good catch — Onyx.merge ignores undefined values, so these fields weren't actually being cleared. Fixed in 2d28ff4: changed all seven fields (from, to, action, payer, exporter, reimbursable, exportedOn) from undefined to null.

Note: the SearchFiltersBar.tsx path still uses undefined and that's correct there — it builds a fresh object passed to buildQueryStringFromFilterFormValues (which converts to a query string), not through Onyx.merge.

Onyx.merge ignores undefined values, so the view-specific filters
(from, to, action, payer, exporter, reimbursable, exportedOn) were
not actually being cleared when changing type through the Advanced
Filters Type page. Using null ensures Onyx properly removes these
keys.

Co-authored-by: Jules <Julesssss@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing check is checklist — it requires a reviewer to post the PR Reviewer Checklist comment on the PR. The error is:

No PR Reviewer Checklist was found

This is unrelated to the code changes in this PR. It's a process check that passes once a reviewer (e.g. @Julesssss) posts the standard reviewer checklist comment. No code fix is needed — a reviewer just needs to copy-paste the reviewer checklist template into a PR comment.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing check is unrelated to the code changes in this PR.

Analysis: The only failing check is checklist, which failed with: No PR Reviewer Checklist was found. This is a process check that requires a reviewer to post the reviewer checklist comment on the PR — it is not caused by any code issue.

Evidence: The PR changes only touch src/components/Search/SearchPageHeader/SearchFiltersBar.tsx and src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTypePage.tsx. All code-related checks (ESLint, Prettier, typecheck, builds, verifySignedCommits, etc.) are passing or pending. The checklist check will resolve once a reviewer posts the reviewer checklist comment.

Copy link
Copy Markdown
Contributor

@Julesssss Julesssss left a comment

Choose a reason for hiding this comment

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

@MelvinBot okay final thing. Could you define a set of those specific keys that will be set to null and reuse in both places them to clean this up.

@Julesssss
Copy link
Copy Markdown
Contributor

Okay that seemed to be applied correctly. Shared some additional review feedback.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing check is unrelated to the code in this PR.

Check: checklist
Error: No PR Reviewer Checklist was found

Analysis: The checklist CI job scans all PR and review comments looking for a "Reviewer Checklist" posted by a reviewer. No reviewer has posted one yet, so the check fails. This is a process gate, not a code issue — no code fix can resolve it.

Resolution: A reviewer (e.g. @JmillsExpensify who is requested) needs to post their Reviewer Checklist comment on this PR. Once posted, the check will pass on the next run.

Comment on lines +315 to +322
updatedFilterFormValues.status = CONST.SEARCH.STATUS.EXPENSE.ALL;
updatedFilterFormValues.from = undefined;
updatedFilterFormValues.to = undefined;
updatedFilterFormValues.action = undefined;
updatedFilterFormValues.payer = undefined;
updatedFilterFormValues.exporter = undefined;
updatedFilterFormValues.reimbursable = undefined;
updatedFilterFormValues.exportedOn = undefined;
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 could you define a set of those specific keys that will be set to null and reuse in both places them to clean this up (here and SearchFiltersTypePage)

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 in 7e06860. Defined VIEW_SPECIFIC_FILTER_KEYS in SearchAdvancedFiltersForm.ts (alongside the existing DATE_FILTER_KEYS and AMOUNT_FILTER_KEYS) and used it in both:

  • SearchFiltersBar.tsx — iterates with for...of and sets each to undefined (this path builds a local object for buildQueryStringFromFilterFormValues, not Onyx directly)
  • SearchFiltersTypePage.tsx — spreads via Object.fromEntries with null values (this path uses Onyx.merge which requires null to clear)

@MelvinBot

This comment was marked as off-topic.

…ge handlers

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

This comment was marked as off-topic.

@Julesssss
Copy link
Copy Markdown
Contributor

@Krishna2323 I think Melvins latest changes make sense, let me know what you think. It followed your initial requests too.

@Krishna2323
Copy link
Copy Markdown
Contributor

Reviewing...

@Krishna2323
Copy link
Copy Markdown
Contributor

The latest changes look good to me as well! Proceeding with testing and completing the checklist.

@Krishna2323
Copy link
Copy Markdown
Contributor

Krishna2323 commented Feb 18, 2026

@MelvinBot I’ve been testing this fix, but I’m not able to reproduce the original bug on the main branch. Here’s what I tried:

  1. Logged in as an auditor (not an approver) in a workspace with multiple members who have expenses
  2. Went to the Submit tab (which has from:[self] action:submit type:expense-report)
  3. Typed filters directly into the search box:
    category:"Meals and Entertainment" date>2025-02-01 date<2025-02-28 tag:12312
  4. Result: All expenses from all submitters were shown — from:[self] was not carried over

I also tried applying date filters through the filter bar UI while staying on the Submit tab, and that correctly keeps from:[self] since the type doesn’t change.

From what I can see, typing into the search box replaces the entire query string, so stale from:[self] isn’t being carried over. Also, the original bug report says “Navigate to Reports > Expenses” — but if the user clicks the Expenses LHN item, they land on a clean type:expense query without any from filter at all.

Question:

  1. Can you share the exact reproduction steps that demonstrate the bug on main (without this fix)? Specifically, which UI interaction carries the stale from:[self] into a new search? Is it the filter bar Type dropdown, the Advanced Filters Type page, or something else?

@Julesssss were you able to reproduce the bug on your end?

Monosnap.screencast.2026-02-18.22-13-14.mp4

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Good catch @Krishna2323. I investigated the code paths in detail and I believe I can explain why you can't reproduce this on main.

The SearchPage.tsx useEffect already prevents the bug

On main, SearchPage.tsx already has this at line 224-227:

const formValues = useFilterFormValues(queryJSON);

// Sync the advanced filters form with the current query when it changes
useEffect(() => {
    updateAdvancedFilters(formValues, true);  // Onyx.set — full replace
}, [formValues]);

This useEffect fires on every query change and calls updateAdvancedFilters with true, which uses Onyx.set() to fully replace the Onyx form. This means:

  1. User lands on Submit view → URL has from:[self]useEffect fires → Onyx form is set to {type: 'expense_report', from: ['12345'], action: 'submit'}
  2. User clicks "Expenses" in the LHN → URL changes to type:expenseuseEffect fires → Onyx form is fully replaced with {type: 'expense'} — stale from is gone
  3. User clicks "Filters" → openAdvancedFilters calls updateAdvancedFilters(filterFormValues) with Onyx.merge → but the Onyx form is already clean, so merge has nothing stale to preserve

The SearchPage.tsx useEffect effectively acts as a cleanup mechanism that prevents the stale Onyx form scenario. By the time the user can interact with the filter bar or open Advanced Filters, the Onyx form has already been synced to match the current URL.

What about the filter bar type-change path?

On main, the updateFilterForm function at SearchFiltersBar.tsx:297-340 starts by spreading searchAdvancedFiltersForm (the Onyx form, line 301) and overlaying new values. On main, when the type changes it clears columns, status, and has — but not from, to, action, etc. However, since the SearchPage.tsx useEffect has already synced the Onyx form to the current URL, searchAdvancedFiltersForm should already be clean.

The only theoretical way stale from could persist is if:

  • The useEffect in SearchPage.tsx hasn't fired yet due to React batching (a very narrow race condition)
  • Or there's a code path that writes to the Onyx form outside of these two mechanisms

What the PR's changes actually protect against

  1. openAdvancedFiltersOnyx.set: Redundant with the SearchPage.tsx useEffect, except as a safety net for a potential React batching race condition.
  2. Clearing VIEW_SPECIFIC_FILTER_KEYS on type change: A defensive improvement to the updateFilterForm function, but similarly should be redundant if the useEffect has already cleaned the form.
  3. SearchFiltersTypePage.tsx changes: Same defensive pattern for the Advanced Filters → Type page path.

Possible reproduction scenario (theoretical)

The only scenario I can construct where this bug could manifest on main:

  1. User is on Submit view (URL has from:[self])
  2. useEffect fires → Onyx form has from:[self]
  3. User changes type via the filter bar dropdown without navigating away first (e.g., changes from "Expense Report" to "Expense" using the type chip on the Submit view)
  4. updateFilterForm starts from searchAdvancedFiltersForm which has from:[self]
  5. On main, the type-change block does NOT clear from
  6. buildQueryStringFromFilterFormValues includes from in the output because from is in ALLOWED_TYPE_FILTERS for the expense type
  7. The resulting URL has type:expense from:[self] — showing only the user's expenses

This is the one path where the type-change clearing of VIEW_SPECIFIC_FILTER_KEYS would actually matter. The reproduction would be:

  1. As an auditor, land on Submit view (from:[self] action:submit type:expense_report)
  2. Without navigating away, change the type chip from "Expense Report" to "Expense"
  3. The from:[self] should persist in the resulting query on main (but be cleared with the PR fix)

Could you try this specific path @Krishna2323? The key is changing the type via the filter bar dropdown while already on the Submit view, not navigating to Expenses first via the LHN.

cc @Julesssss — Can you confirm this is the reproduction path you observed?

@Julesssss
Copy link
Copy Markdown
Contributor

Hmm good question. QA are also not able to reproduce this bug.

That's my fault, I picked an issue for us to test and din't notice that it hadn't been reproduced by QA originally 😖

Reproducible in staging?: Needs Reproduction (Reproduction blocked)
Reproducible in production?: Needs Reproduction

@Julesssss
Copy link
Copy Markdown
Contributor

Okay lets call this an edge case and drop the potential fix. It's not worth the fix currently.

@Krishna2323 we can pay out the full bounty, thanks for your help here

@Julesssss Julesssss closed this Feb 18, 2026
@Julesssss Julesssss deleted the claude-fixAuditorFilterPersistence branch February 18, 2026 23:47
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.

3 participants