Skip to content

[$175] Search - An asterisk (*) is added after merchant in the search query #104299

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: v9.5.13-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/79083047/folder/57529521/41237511/1869287748?sort=%2Bidentifier
Email or phone of affected tester (no customers): n/a
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: Mac 26.5.1 Safari, iPhone 13 iOS 26.6
App Component: Search

Action Performed:

  1. Go to https://staging.new.expensify.com
  2. Go to the workspace chat
  3. Submit at least 1 expense with the merchant set to “Coffee”
  4. Click on the search 🔍 icon
  5. Search for “merchant:coffee”
  6. Click on the search 🔍 icon again

Expected Result:

The search query should be displayed without an additional asterisk

Actual Result:

An asterisk (*) is added after merchant in the search query

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7288233_1792164759090.Recording__3149.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7288233

Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 17, 2026
  2. applause-bot commented on Oct 17, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/2494367aa2d9cfb3d6e8627ad7798848b142c944243a6169d06866215fdeef20

  3. MelvinBot commented on Oct 17, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Add Contains / Equal to match type for the Merchant filter (merged 2026-10-14) introduced this regression. It added merchant*: as the syntax for a "Contains" match.

    • The parser reads a typed merchant:coffee as a "Contains" filter. It does not keep the : the user typed.
    • When the app turns the query back into text, it always writes "Contains" as *:.
    • When you submit, the router rewrites the query this way. When you reopen the router, it fills the input from the same rebuilt text.

    So merchant:coffee comes back as merchant*:coffee. I reproduced this on web: the reopened router shows type:expense merchant*:coffee.

    Search router reopened showing merchant*:coffee

    Code references

    Merchant is the only filter affected. *: on any other key falls back to a keyword search.

    What changes do you think we should make in order to solve the problem?

    Write merchant "Contains" as merchant: instead of merchant*: when building query text.

    • In buildFilterValuesString, use : when the filter is merchant and the operator is contains.
    • In buildMerchantFilterQuery, use : for the Contains match type.
    • Keep *: in the grammar, so typed merchant*:x still works. It will be shown as merchant:x, which means the same thing.
    • Update the unit tests in tests/unit/Search/SearchQueryUtilsTest.ts that expect merchant*:.

    The round trip still works, because merchant: already parses as Contains and merchant= stays exact. The query you see then matches what you typed. Saved-search hashes for merchant queries also return to their values from before the PR.

    What alternative solutions did you explore? (Optional)

    Change only the display. Keep merchant*: as the stored form, and show merchant: only in buildUserReadableQueryString. This keeps the current hashes, but the URL would still show merchant*:.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](#proposal) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run · view recording

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 17, 2026
  5. j3ffffff commented on Oct 17, 2026

    @j3ffffff

    VERDICT: POST
    ROOT_CAUSE_ONELINER: buildFilterValuesString in SearchQueryUtils.ts serializes the merchant + CONTAINS operator as merchant*:coffee (using operatorToCharMap[CONTAINS] = *:), but the user-facing legacy syntax for merchant contains is merchant: (no asterisk) — so every round-trip through parse→display adds the asterisk.
    PROPOSAL_TYPE: FIRST
    AUTOPOST_SAFE: YES

  6. melvin-bot commented on Oct 17, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @rojiphil (External)

  7. changed the title [-]Search - An asterisk (*) is added after merchant in the search query[/-] [+][$175] Search - An asterisk (*) is added after merchant in the search query[/+] on Oct 17, 2026
  8. melvin-bot commented on Oct 17, 2026

    @melvin-bot
  9. camesenin commented on Oct 17, 2026

    @camesenin

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01b60d704ba377be83

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When searching for an expense using the merchant filter (e.g., merchant:coffee), reopening or re-submitting the search query in the search bar automatically appends an asterisk (*) to the merchant filter, changing it to merchant*:coffee.

    What is the root cause of that problem?

    In PR #83733, operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS] was defined as *: in src/libs/SearchQueryUtils.ts:99.

    1. The search parser correctly treats merchant:coffee as a positive contains filter.
    2. However, when rebuilding the user-facing query string in buildFilterValuesString (SearchQueryUtils.ts:676-680) and buildMerchantFilterQuery (SearchQueryUtils.ts:156), the serialization logic pulls operatorToCharMap[queryFilter.operator] for CONTAINS, outputting merchant*: instead of the standard canonical merchant prefix merchant:.
    3. When the search router reopens, SearchRouter.tsx reconstructs the input text via buildUserReadableQueryString, reading the serialized string and displaying merchant*:coffee.

    What changes do you think we should make in order to solve the problem?

    In src/libs/SearchQueryUtils.ts:

    1. In buildMerchantFilterQuery (line 156), write the default positive CONTAINS operator as ::
    if (!isNegated) {
        operator = selectedMerchantOperator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO ? EXPLICIT_EQUAL_TO_OPERATOR : operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO];
    }
    1. In buildFilterValuesString (lines 676-680), serialize merchant CONTAINS as : instead of *::
    const operatorChar =
        filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT
            ? queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO
                ? EXPLICIT_EQUAL_TO_OPERATOR
                : operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO]
            : operatorToCharMap[queryFilter.operator];
    1. Keep *: in src/libs/SearchParser/searchParser.peggy for backward compatibility so typed queries containing *: continue to parse as contains, while ensuring serialization always outputs the clean merchant: syntax.
    2. Update unit test assertions in tests/unit/Search/SearchQueryUtilsTest.ts.

    What alternative solutions did you explore? (Optional)

    Display-only conversion in buildUserReadableQueryString: rejected because keeping merchant*: in the internal serialized query string would still leak the asterisk into the URL route and break saved search query hashes.

    • Platform Verification:
      • Web (Chrome / Safari)
      • Mobile Web (iOS / Android)
      • Desktop (MacOS / Windows)
  10. melvin-bot commented on Oct 17, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. zackthompson317 commented on Oct 18, 2026

    @zackthompson317

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    After searching merchant:coffee, reopening the search router shows merchant*:coffee. The query the user typed gets rewritten with an asterisk they never entered.

    What is the root cause of that problem?

    I agree with the regression source (#83733). I want to point out where the *: actually comes from, because it reaches more than the router text.

    1. The parser keeps the legacy meaning of a positive merchant: by turning it into contains (searchParser.peggy:246-249).
    2. Every place that turns a contains filter back into text reads one map, operatorToCharMap, and that map says CONTAINS: '*:' (SearchQueryUtils.ts:97-100). It is used by buildFilterValuesString (L676-683) and buildMerchantFilterQuery (L154-157).
    3. buildFilterValuesString isn't only used for display. getQueryHashes and getQueryHashWithoutFilters build the hash string with it too (L946-1057). So the asterisk also changed the hash of every existing merchant: query. That hash keys saved searches and search snapshots.

    You can see this in the test that guards hash stability. Before #83733, buildSearchQueryJSON('type:expense merchant:Amazon').hash was pinned to 1692688529 (parent commit a4e971f, SearchQueryUtilsTest.ts:2739). #83733 changed the expectation to 1712664595 and added the comment "merchant: parses as contains and hashes as merchant*:" (SearchQueryUtilsTest.ts:2948-2954). I rebuilt the hash input with the same hashCode function. The two values come from type:expense status: groupBy:undefined merchant:Amazon sortBy:date sortOrder:desc columns:undefined, with and without the *. So the extra * is the only reason the hash changed.

    What changes do you think we should make in order to solve the problem?

    Fix it where the *: comes from, in operatorToCharMap, instead of adding merchant special cases at each place that writes the query:

     const operatorToCharMap = {
         [CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO]: ':' as const,
    -    [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: '*:' as const,
    +    // A positive `merchant:` already parses as contains (`merchant=` is the exact match), so write contains as `:`.
    +    // `*:` stays accepted by the parser for queries and URLs saved since #83733.
    +    [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: ':' as const,

    Why this is safe:

    • Only merchant can hold contains in a parsed query. The parser turns contains on any other key, or a negated one, into a keyword (peggy:219-222, 251-254). Description's eq to contains swap happens only in serializeQueryJSONForBackend, which never writes text. So the change can't alter any other filter.
    • Exact match still writes =, through the EXPLICIT_EQUAL_TO_OPERATOR branch, so merchant= and merchant: still round-trip as two different filters.
    • Both branches of buildMerchantFilterQuery (the new-clause one at L154-157 and the clause-rewrite one that calls buildFilterValuesString) pick up : automatically. The router text, URL q, pills, hashes, and the Merchant filter form all agree with no extra branches.
    • The *: token stays in the grammar. Links, recent searches, and saved-search queries written as merchant*:x between 10/14 and the fix still parse as contains, and they show up as merchant:x.
    • Hashes for merchant: queries go back to their pre-[Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733 values. That is exactly what the hash-stability test is meant to guarantee, because these hashes key saved searches and snapshots.

    Then update the merchant*: output expectations in tests/unit/Search/SearchQueryUtilsTest.ts to merchant:. Inputs that use merchant*: stay as they are, because they still parse.

    What specific scenarios should we cover in automated tests?

    • Restore expect(buildSearchQueryJSON('type:expense merchant:Amazon')?.hash).toEqual(1692688529) (the pre-[Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733 value) and drop the drift comment.
    • Round trip: buildUserReadableQueryString(buildSearchQueryJSON('type:expense merchant:coffee')) returns type:expense merchant:coffee. getQueryWithUpdatedValues('merchant:coffee') contains no *.
    • merchant*:coffee still parses as contains and comes back as merchant:coffee, with the same hash as merchant:coffee.
    • merchant=coffee still comes back as merchant=coffee, with a different hash from merchant:coffee.
    • Merchant filter form set to "Contains" builds merchant:coffee, and "Exactly matches" builds merchant=coffee.

    What alternative solutions did you explore? (Optional)

    • Special-casing merchant contains inside buildFilterValuesString and buildMerchantFilterQuery, as suggested above. It works, but it leaves the map saying contains is *:, so any future serializer that reads the map brings the bug back. It also needs the same fix in two places.
    • Fixing only buildUserReadableQueryString or the router. That leaves * in the URL and in the hashes, so the hash drift stays.

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~010ec78449791d4046

  12. melvin-bot commented on Oct 18, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  13. 1 remaining item

  14. emkhalid commented on Oct 18, 2026

    @emkhalid
    Contributor

    @rojiphil! I was able to reproduce this issue on both macOS Chrome and iOS Native, and have verified a clean 1-line fix for it. Before posting a full proposal, I wanted to point out why MelvinBot's proposed solution is suboptimal.

    Why MelvinBot's Solution Isn't Ideal

    MelvinBot suggests leaving operatorToCharMap[CONTAINS] set to '*:' and adding duplicate if (merchant && contains) special-case checks inside multiple helper functions (buildFilterValuesString and buildMerchantFilterQuery).

    This leaves the central operator map inaccurate, duplicates logic, and risks future regressions if other query builders read from the map.

    The Cleaner Approach (1-Line Fix)

    The canonical user-facing query character for merchant contains has always been : (exact match explicitly uses =).

    The root issue is simply that operatorToCharMap[CONTAINS] was mapped to '*:'. Changing that single entry to ':' in src/libs/SearchQueryUtils.ts:

    • Automatically fixes all serialization paths (search router, URL parameter, and filter popups) with zero special-case branching.
    • Automatically restores saved search query hash stability to its pre-regression state.
    • Preserves backward compatibility because baseRules.peggy still accepts *: for any existing saved queries.

    Demo Videos

    macOS Web Chrome (Repro & Fix):

    Repro:

    merchant-mac-repro.mov

    Fix:

    merchant-mac-fix.mov

    iOS Native (Repro & Fix):

    Repro:

    merchant-ios-repro.mov

    Fix:

    merchant-ios-fix.mov

    Would you like me to submit the formal proposal with these demo videos and test cases?

  15. sijo0218 commented on Oct 18, 2026

    @sijo0218

    @rojiphil Following up on my proposal above:

    To keep the solution as minimal and clean as possible with zero branching or helper changes, the cleanest approach is updating operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS] directly to ':' in src/libs/SearchQueryUtils.ts:

    const operatorToCharMap = {
        [CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO]: ':' as const,
    -   [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: '*:' as const,
    +   [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: ':' as const,

    Since CONTAINS is used for partial text matching (which canonically uses the : operator across user-facing search syntax while exact matches use =), this 1-line update cleanly preserves merchant:coffee across all serialization paths with zero redundant conditionals.

  16. jloa-dev commented on Oct 19, 2026

    @jloa-dev

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When searching for an expense using the search query merchant:coffee and clicking the search icon again, an asterisk (*) is automatically appended to the search query (becoming merchant:coffee*).

    What is the root cause of that problem?

    In src/libs/SearchQueryUtils.ts:
    When building the search query string from autocomplete suggestions or re-serializing the query for the search input bar, the query serializer checks if the merchant filter was matched via partial string matching. In buildSearchQueryString, wildcard asterisks are injected for partial matches. This logic was intended for background autocomplete calls to the backend, but it erroneously leaked into the user-facing search query input component, mutating the user's explicit input string upon subsequent clicks or focuses.

    What changes do you think we should make in order to solve the problem?

    In src/libs/SearchQueryUtils.ts, ensure that user-entered search queries in the search input retain their literal string without automatically appending asterisks:

    function formatSearchQueryForDisplay(query: string): string {
        return query.replace(/(merchant:[^\s*]+)\*/g, '$1');
    }

    And in SearchAutocompleteInput.tsx, ensure the display text only displays the exact sanitized user value.

    What specific scenarios should we cover in automated tests?

    In tests/unit/SearchQueryUtilsTest.ts:

    • Verify that searching for merchant:coffee and re-rendering the search bar maintains merchant:coffee without appending *.
  17. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  18. azhumide commented on Oct 19, 2026

    @azhumide

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01096eb540e2a8e9a5

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When searching for an expense using the merchant filter (e.g., merchant:coffee), reopening or re-submitting the search query in the search bar automatically appends an asterisk (*) to the merchant filter, rewriting it as merchant*:coffee.

    What is the root cause of that problem?

    In PR #83733, operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS] was mapped to '*:' in src/libs/SearchQueryUtils.ts:99.

    1. The search parser correctly treats merchant:coffee as a positive contains filter.
    2. However, when rebuilding/serializing the user-readable query string in buildFilterValuesString (SearchQueryUtils.ts:676-680) and buildMerchantFilterQuery (SearchQueryUtils.ts:156), the serialization logic reads operatorToCharMap[queryFilter.operator], which outputs *: instead of the standard canonical merchant prefix :.
    3. Consequently, whenever the search router reopens, SearchRouter.tsx reconstructs the input text via buildUserReadableQueryString and displays merchant*:coffee.

    What changes do you think we should make in order to solve the problem?

    The canonical user-facing query character for merchant contains is : (exact match explicitly uses =).

    In src/libs/SearchQueryUtils.ts:
    Update operatorToCharMap[CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS] directly from '*:' to ':':

    const operatorToCharMap = {
        [CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO]: ':' as const,
    -   [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: '*:' as const,
    +   [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: ':' as const,

    Why this is the cleanest, minimal solution:

    • Automatically fixes all serialization paths (search router input, URL q parameter, and filter popups) without any redundant special-casing or duplicate branches.
    • Restores saved search query hash stability back to its pre-regression state.
    • Backward-compatible: baseRules.peggy still accepts *: for existing saved queries while serialization always outputs the clean canonical merchant: syntax.
    • Update unit tests in tests/unit/Search/SearchQueryUtilsTest.ts to expect merchant: instead of merchant*:.

    What specific scenarios should we cover in automated tests?

    In tests/unit/Search/SearchQueryUtilsTest.ts:

    • Verify buildUserReadableQueryString(buildSearchQueryJSON('type:expense merchant:coffee')) returns type:expense merchant:coffee.
    • Verify merchant*:coffee still parses as contains and normalizes to merchant:coffee.
    • Verify merchant=coffee still produces exact match without regression.
  19. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ⚠️ Invalid email. Please make sure to create an Expensify account with this email first here.

  20. azhumide commented on Oct 19, 2026

    @azhumide

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01096eb540e2a8e9a5

  21. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  22. HiepPP commented on Oct 19, 2026

    @HiepPP

    hi @rojiphil @neil-marcellini, I'm the author of #83733 and this bug comes from my PR, sorry for that. Can I take it as a follow-up PR? No payment needed.

    In #83733 I made a plain merchant: mean contains, and kept merchant*: as another way to type it. But the serializer still maps contains to *:, so whenever the app rebuilds the query string, the user's merchant:coffee turns into merchant*:coffee. I missed this case in that PR.

    I'll go with the one-line change that was already suggested above: map contains to : in operatorToCharMap. The grammar is not touched, so a query with merchant*: is still valid, it is just displayed without the * after that. merchant=coffee (exact match) is not affected. This also reverts the hash value I had to update in the hash test in #83733.

    One other behavior change from this, could you confirm it's OK? Picking Contains in the Merchant filter will write merchant:Coffee instead of merchant*:Coffee. The results are the same.

    The fix and the test updates are ready on my side. I also checked it on web:

    • merchant:coffee stays the same when I open the router again
    • Contains and Equal to return the right expenses
    • the Top merchants drilldown still uses exact match

    Could you please confirm so I can open the PR? Thank you

  23. bonnhatnguyen commented on Oct 19, 2026

    @bonnhatnguyen

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When searching for expenses using the merchant filter (e.g. typing merchant:coffee), upon submitting the search and reopening the Search Router bar, the query automatically mutates and appends an unwanted asterisk * after merchant, displaying merchant*:coffee instead of merchant:coffee.


    What is the root cause of that problem?

    1. Introduction of *: syntax in PR [Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733:
      In PR #83733 ("Add Contains / Equal to match type for the Merchant filter"), *: was introduced into operatorToCharMap to represent the CONTAINS operator:
      In src/libs/SearchQueryUtils.ts (line 99):

      const operatorToCharMap: Record<SearchSyntaxOperator, string> = {
          [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: '*:',
          ...
      };
    2. Parser vs Serializer Asymmetry:

      • In src/libs/SearchParser/searchParser.peggy (lines 246–249), the parser parses standard merchant:value as operator: 'contains'.
      • When the search query is executed, SearchRouter.tsx reconstructs and serializes the user-readable query string by invoking buildUserReadableQueryString and buildFilterValuesString.
      • In src/libs/SearchQueryUtils.ts (lines 676–683):
        const operatorChar = operatorToCharMap[queryFilter.operator];
        const filterValuesString = `${filterName}${operatorChar}${serializedValues}`;
        Because queryFilter.operator is CONTAINS, operatorToCharMap[CONTAINS] blindly injects *:, transforming merchant:coffee into merchant*:coffee.
      • Likewise, in buildMerchantFilterQuery (SearchQueryUtils.ts:154-158), the Contains match type serializes using operatorToCharMap[selectedMerchantOperator], emitting merchant*:.
    3. Reopening Search Router:
      When the user reopens the Search Router (SearchRouter.tsx:140-179), the input value is prefilled from the rebuilt query string (buildUserReadableQueryString(currentSearchQuery)), exposing the unwanted merchant*:coffee text.


    What changes do you think we should make in order to solve the problem?

    In src/libs/SearchQueryUtils.ts:

    1. Serialize Merchant Contains as Standard : in buildFilterValuesString:
      When serializing the merchant filter, the default positive CONTAINS operator should format using standard : (operatorToCharMap[EQUAL_TO] which is :), keeping *: reserved only if an explicit non-default syntax is specified, or normalizing all positive merchant contains queries to the clean : prefix:

      --- a/src/libs/SearchQueryUtils.ts
      +++ b/src/libs/SearchQueryUtils.ts
      @@ -675,7 +675,10 @@ function buildFilterValuesString(filterName: string, queryFilter: QueryFilter):
           }
      
      -    const operatorChar = operatorToCharMap[queryFilter.operator];
      +    const operatorChar =
      +        filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT && queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS
      +            ? ':'
      +            : operatorToCharMap[queryFilter.operator];
           const filterValuesString = `${filterName}${operatorChar}${serializedValues}`;
           return filterValuesString;
       }
    2. Align buildMerchantFilterQuery (SearchQueryUtils.ts):

      --- a/src/libs/SearchQueryUtils.ts
      +++ b/src/libs/SearchQueryUtils.ts
      @@ -154,5 +154,5 @@ function buildMerchantFilterQuery(merchantFilter: SearchMerchantFilter): string
           if (!isNegated) {
      -        operator = operatorToCharMap[selectedMerchantOperator];
      +        operator = selectedMerchantOperator === CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS ? ':' : operatorToCharMap[selectedMerchantOperator];
           } else {
               operator = selectedMerchantOperator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO ? EXPLICIT_EQUAL_TO_OPERATOR : operatorToCharMap[selectedMerchantOperator];
           }
    3. Backward Compatibility:
      Keep *: in searchParser.peggy so that if a user or bookmark already has merchant*:, it still parses cleanly as CONTAINS. The round-trip rebuild will simply format it cleanly as merchant:.

    4. Unit Tests:
      Update unit tests in tests/unit/Search/SearchQueryUtilsTest.ts to assert that merchant:value preserves the : prefix on round-trip serialization and router reopening.


    What alternative solutions did you explore? (Optional)

    • Modifying only buildUserReadableQueryString for display: Rejected because keeping merchant*: in the internal query string leaves the asterisk in the URL route and breaks saved search query hash consistency.

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  24. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  25. emkhalid commented on Oct 20, 2026

    @emkhalid
    Contributor

    @camesenin, @zackthompson317, @j3ffffff, @sijo0218, @jloa-dev, @azhumide and @bonnhatnguyen
    If you're human contributors and not automation bots, please read the Contributing Guidelines and wait for the Help Wanted label before posting proposals.

    4. Proposals must only be posted after the `Help Wanted` label is added. Any proposals submitted beforehand will be ignored and not reviewed. Do not post proposals in Slack. MelvinBot is an exception: it may post a proposal before `Help Wanted` is applied.

    Find issues actually open for proposals here:
    https://github.com/Expensify/App/issues?q=is%3Aissue+state%3Aopen+label%3A%22Help+Wanted%22

  26. rojiphil commented on Oct 20, 2026

    @rojiphil
    Contributor

    Not due yet.
    Awaiting clarity at #80823 (comment) on whether this is a regression or an improvement.

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

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions