Skip to content

[$250] [Due for payment 2026-04-30] Search - The workspace with the same name is not displayed in the autocomplete #82304

Description

@jponikarchuk

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: v9.3.18-1
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/TR-5134/folder/13176922/47701683/1186708606?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 15.4.1 Safari, Chrome, iPhone 13 iOS 26.2.1
App Component: Search

Action Performed:

  1. Open https://staging.new.expensify.com
  2. Create 2 workspaces with the same name
  3. Go to the Reports tab
  4. On the search type "workspace:" and Verify that you see autocomplete list for workspaces
  5. Select one workspace, and verify that the results are updated
  6. Add a comma after the selected workspace

Expected Result:

Another workspace with the same name should appear in the autocomplete for the workspaces

Actual Result:

The workspace with the same name is not displayed in the autocomplete

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

1.mp4

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022050309179163591879
  • Upwork Job ID: 2050309179163591879
  • Last Price Increase: 2026-05-01

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Feb 12, 2026
  2. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Feb 12, 2026
  3. melvin-bot commented on Feb 12, 2026

    @melvin-bot

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

  4. melvin-bot commented on Feb 12, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  5. melvin-bot commented on Feb 12, 2026

    @melvin-bot

    Triggered auto assignment to @twisterdotcom (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

  6. daledah commented on Feb 12, 2026

    @daledah
    Contributor

    Proposal

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

    The workspace with the same name is not displayed in the autocomplete.

    What is the root cause of that problem?

    The root cause is in the SearchAutocompleteList component. When filtering workspaces for autocomplete suggestions, the code uses workspace names (which can be duplicated) instead of workspace IDs (which are unique) to determine which workspaces have already been selected.

    The alreadyAutocompletedKeys Set is created from range.value.toLowerCase() which contains the workspace name:

    const alreadyAutocompletedKeys = new Set(
    ranges
    .filter((range) => {
    return autocompleteKey && range.key === autocompleteKey;
    })
    .map((range) => range.value.toLowerCase()),
    );

    Then in the POLICY_ID case, the filter checks against workspace.name.toLowerCase():

    case CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID: {
    const filteredPolicies = workspaceList
    .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.name.toLowerCase()))
    .sort()
    .slice(0, 10);
    return filteredPolicies.map((workspace) => ({
    filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID,
    text: workspace.name,
    autocompleteID: workspace.id,
    mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID,
    }));
    }

    When two workspaces have the same name, selecting one workspace causes the other to be filtered out because they share the same name.

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

    We should modify the POLICY_ID case to build a Set of already-selected workspace IDs instead of names, then filter by checking if workspace.id is already in that Set.

     case CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID: {
    +    const alreadyAutocompletedPolicyIDs = new Set(
    +        ranges
    +            .filter((range) => range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID)
    +            .flatMap((range) =>
    +                workspaceList.filter((workspace) => workspace.name.toLowerCase() === range.value.toLowerCase()).map((workspace) => workspace.id),
    +            ),
    +    );
         const filteredPolicies = workspaceList
    -        .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.name.toLowerCase()))
    +        .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedPolicyIDs.has(workspace.id))
             .sort()
             .slice(0, 10);
    
         return filteredPolicies.map((workspace) => ({
             filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID,
             text: workspace.name,
             autocompleteID: workspace.id,
             mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID,
         }));
     }

    What alternative solutions did you explore? (Optional)

    An alternative would be to pass the substitutionMap prop (which contains mapKey -> autocompleteID mappings) into SearchAutocompleteList and use it directly to get the IDs of already-selected workspaces. However, this would require modifying the component's interface and all its consumers, making it a more invasive change.

  7. abbasifaizan70 commented on Feb 12, 2026

    @abbasifaizan70
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-02-18 12:01:13 UTC.

    Proposal

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

    The workspace with the same name is not displayed in the autocomplete. With 3+ workspaces sharing the same name, the issue is more visible: after selecting one or more "A's Workspace" entries, the remaining same-name workspaces still do not appear correctly, or the wrong ones are excluded.


    What is the root cause of that problem?

    In SearchAutocompleteList.tsx, the workspace (POLICY_ID) autocomplete excludes already-selected options using workspace name via alreadyAutocompletedKeys.has(workspace.name.toLowerCase()). That set is built from the display values already present in the query (e.g. "My Workspace"). So after the user selects one workspace named "My Workspace", every workspace whose name matches is treated as "already selected" and filtered out. The second (and third, etc.) workspace with the same name is therefore hidden even though it is a different policy (different ID).

    Additionally, the substitution map used to store selected workspace IDs is keyed only by display name (policyID:A's Workspace). When the user selects a second or third "A's Workspace", the new selection overwrites the same key, so only one policy ID is ever stored. As a result:

    • The actual search query (after substituting names with IDs) only ever targets one workspace.
    • Autocomplete cannot exclude by ID because it only has one ID in the map; it cannot distinguish which same-name workspaces are already selected when there are 3+.

    So the root cause has two parts:

    1. Exclusion by name instead of ID — filtering uses alreadyAutocompletedKeys.has(workspace.name.toLowerCase()), so all workspaces with that name are hidden.
    2. Single substitution per name — the substitution map has only one entry per (filterKey, value) (e.g. policyID:A's Workspace), so multiple same-name selections overwrite each other and only one policy ID is kept.

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

    Exclude workspace options by policy ID (when substitution data is available) instead of by name, and support multiple selections of the same display name so that 3+ workspaces with the same name can each be selected and stored correctly.

    1. Index-based substitution keys — When the same filter+value appears multiple times in the query (e.g. workspace:"A's Workspace", "A's Workspace", "A's Workspace"), store each selection under a unique key: first occurrence uses the existing key (e.g. policyID:A's Workspace), subsequent occurrences use policyID:A's Workspace:1, policyID:A's Workspace:2, etc. This allows multiple policy IDs for the same display name and correct query substitution for each occurrence.

    2. SearchAutocompleteList — Accept an optional autocompleteSubstitutions prop. For the POLICY_ID (workspace) case, build a set of already-selected policy IDs from the current query using the substitution map (with index-based keys). Filter out workspaces whose id is in that set. Fall back to name-based exclusion when no substitutions are available.

    3. SearchRouter / SearchPageHeaderInput — When the user selects a workspace from autocomplete, parse the new query (after appending the selected workspace name), determine the occurrence index for that filter+value, and store the substitution under the corresponding index-based key so we do not overwrite previous same-name selections.

    4. getQueryWithSubstitutions / getUpdatedSubstitutionsMap — Use the same index-based key scheme when substituting query text with IDs and when pruning the substitution map so that each occurrence of the same workspace name is correctly mapped to its chosen policy ID.


    Implemented changes (for PR)

    The following changes were made to fix the issue and support 3+ workspaces with the same name.

    1. App/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts

    • Added getSubstitutionMapKeyWithIndex(filterKey, value, index): for index 0 returns the existing key (e.g. policyID:A's Workspace); for index 1, 2, ... returns policyID:A's Workspace:1, policyID:A's Workspace:2, etc.
    • Updated getQueryWithSubstitutions: assigns an occurrence index to each range (per key+value), then looks up substitution using the index-based key so each occurrence in the query is replaced by the correct policy ID. Keeps backward compatibility by falling back to the non-indexed key for index 0.
    • Exported getSubstitutionMapKeyWithIndex for use in other modules.

    2. App/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts

    • Updated to assign an occurrence index to each range when building the updated map from the current query.
    • Preserves all index-based keys (baseKey, baseKey:1, baseKey:2, ...) when pruning so that multiple same-name workspace selections remain in the map and are not dropped.

    3. App/src/components/Search/SearchRouter/SearchRouter.tsx

    • Imported parse from the autocomplete parser and getSubstitutionMapKeyWithIndex from getQueryWithSubstitutions.
    • When the user selects an autocomplete suggestion (workspace): after building the new query string, parses it and counts how many ranges share the same filter key and value; sets the substitution key for the last occurrence (index = count − 1) using getSubstitutionMapKeyWithIndex so the new selection is stored under a unique key and does not overwrite previous same-name selections.
    • Passes autocompleteSubstitutions={autocompleteSubstitutions} to SearchAutocompleteList.

    4. App/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx

    • Imported parse from the autocomplete parser, getSubstitutionMapKeyWithIndex, and SearchFilterKey type.
    • Applied the same index-based substitution key logic when the user selects a workspace from autocomplete (parse new query, compute index, set substitution with index-based key).
    • Passes autocompleteSubstitutions={autocompleteSubstitutions} to both SearchAutocompleteList usages.

    5. App/src/components/Search/SearchAutocompleteList.tsx

    • Added optional prop autocompleteSubstitutions?: SubstitutionMap and imported SubstitutionMap and getSubstitutionMapKeyWithIndex.
    • POLICY_ID (workspace) case:
      • Builds alreadySelectedPolicyIds by iterating over ranges with key POLICY_ID, computing the same occurrence index per (key, value), and reading the policy ID from autocompleteSubstitutions using the index-based key (with fallback for index 0 to the non-indexed key).
      • Filters workspaces with !alreadySelectedPolicyIds.has(workspace.id) when substitutions are available, so only already-selected policies are excluded and all other workspaces (including 3+ with the same name) remain in the list until selected.
      • Falls back to the existing name-based exclusion (alreadyAutocompletedKeys.has(workspace.name.toLowerCase())) when autocompleteSubstitutions is not provided or yields no IDs.

    With these changes, selecting multiple workspaces with the same name (e.g. 3× "A's Workspace") stores each selection under a distinct key, the search query substitutes each occurrence with the correct policy ID, and the autocomplete list hides only the workspaces that are already in the query (by ID), so the remaining same-name options stay visible for selection.


    PR Link

    #82784

    Demo

    Screen.Recording.2026-02-18.at.4.59.03.PM.mov
  8. github-actions commented on Feb 12, 2026

    @github-actions
    Contributor

    ⚠️ @abbasifaizan70 Your proposal is a duplicate of an already existing proposal and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.

  9. nkdengineer commented on Feb 13, 2026

    @nkdengineer
    Contributor

    Proposal

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

    When two workspaces share the same name, the autocomplete list for the workspace: filter only allows selecting one of them. After selecting the first workspace and adding a comma to select another, the second workspace with the same name does not appear in the autocomplete suggestions.

    What is the root cause of that problem?

    The alreadyAutocompletedKeys set is built from the parsed query ranges using the display text (range.value) instead of the unique ID of each item:

    const alreadyAutocompletedKeys = new Set(
        ranges
            .filter((range) => {
                return autocompleteKey && range.key === autocompleteKey;
            })
            .map((range) => range.value.toLowerCase()),
    );

    ranges
    .filter((range) => {
    return autocompleteKey && range.key === autocompleteKey;
    })
    .map((range) => range.value.toLowerCase()),
    );

    For the POLICY_ID case, range.value is the workspace name. When two workspaces have the same name (e.g., "My Workspace"), selecting the first one adds "my workspace" to alreadyAutocompletedKeys. The filter then excludes ALL workspaces matching that name:

    case CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID: {
        const filteredPolicies = workspaceList
            .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.name.toLowerCase()))
            .sort()
            .slice(0, 10);

    .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.name.toLowerCase()))
    .sort()
    .slice(0, 10);
    return filteredPolicies.map((workspace) => ({
    filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID,
    text: workspace.name,
    autocompleteID: workspace.id,
    mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID,
    }));
    }
    case CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION: {
    const filteredActionTypes = Object.values(CONST.SEARCH.ACTION_FILTERS).filter((actionType) => {

    The actual unique IDs are already tracked in autocompleteSubstitutions (a SubstitutionMap of {filterKey:displayValue → uniqueID}) inside SearchPageHeaderInput, but this data is never passed down to SearchAutocompleteList:

    const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState<SubstitutionMap>({});
    const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false);

    const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
    setAutocompleteSubstitutions(substitutions);
    }

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

    Pass autocompleteSubstitutions down from SearchPageHeaderInput to SearchAutocompleteList, then use it to compute alreadyAutocompletedKeys based on unique IDs instead of display text for filter types that support autocompleteID (like POLICY_ID).

    1. In src/components/Search/SearchAutocompleteList.tsx, add autocompleteSubstitutions to SearchAutocompleteListProps:
    type SearchAutocompleteListProps = {
        // ... existing props ...
        /** Map of autocomplete substitutions (filterKey:displayValue → uniqueID) */
        autocompleteSubstitutions?: SubstitutionMap;
    };

    type SearchAutocompleteListProps = {
    /** Value of TextInput */
    autocompleteQueryValue: string;
    /** Callback to trigger search action * */
    handleSearch: (value: string) => void;
    /** An optional item to always display on the top of the router list */
    searchQueryItem?: SearchQueryItem;
    /** Any extra sections that should be displayed in the router list. */
    getAdditionalSections?: GetAdditionalSectionsCallback;
    /** Callback to call when an item is clicked/selected */
    onListItemPress: (item: OptionData | SearchQueryItem) => void;
    /** Callback to call when user did not click an item but still text query should be changed */
    setTextQuery: (item: string) => void;
    /** Callback to call when the list of autocomplete substitutions should be updated */
    updateAutocompleteSubstitutions: (item: SearchQueryItem) => void;
    /** Whether to subscribe to KeyboardShortcut arrow keys events */
    shouldSubscribeToArrowKeyEvents?: boolean;
    /** Callback to highlight (e.g. scroll to) the first matched item in the list. */
    onHighlightFirstItem?: () => void;
    /** Ref for textInput */
    textInputRef?: React.RefObject<AnimatedTextInputRef | null>;
    /** Personal details */
    personalDetails: OnyxEntry<PersonalDetailsList>;
    /** Reports */
    reports: OnyxCollection<Report>;
    /** All feeds */
    allFeeds: Record<string, CardFeeds | undefined> | undefined;
    /** All cards */
    allCards: CardList | undefined;
    /** Reference to the outer element */
    ref?: ForwardedRef<SelectionListHandle>;
    };

    1. In the same file, when computing alreadyAutocompletedKeys, resolve the values to their unique IDs using the substitution map. For the POLICY_ID case, filter by workspace.id against the set of already-selected IDs instead of filtering by workspace.name.
    const alreadyAutocompletedKeys = new Set(
        ranges
            .filter((range) => autocompleteKey && range.key === autocompleteKey)
            .map((range) => {
                const mapKey = getSubstitutionMapKey(range.key, range.value);
                return autocompleteSubstitutions?.[mapKey] ?? range.value.toLowerCase();
            }),
    );
    
    // ...
    case CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID: {
        const filteredPolicies = workspaceList
            .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.id))
            .sort()
            .slice(0, 10);

    ranges
    .filter((range) => {
    return autocompleteKey && range.key === autocompleteKey;
    })
    .map((range) => range.value.toLowerCase()),
    );

    .filter((workspace) => workspace.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(workspace.name.toLowerCase()))
    .sort()
    .slice(0, 10);
    return filteredPolicies.map((workspace) => ({
    filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID,
    text: workspace.name,
    autocompleteID: workspace.id,
    mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID,
    }));
    }
    case CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION: {
    const filteredActionTypes = Object.values(CONST.SEARCH.ACTION_FILTERS).filter((actionType) => {

    After this, we need to refactor all other cases to use the value we mapped to autocompleteID to filter out with alreadyAutocompletedIDs instead, because the bug also happens for other keys example like In, we need to check chat.reportID is included in alreadyAutocompletedKeys instead of chat.text

    1. In src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx, pass autocompleteSubstitutions to both SearchAutocompleteList render sites:
    <SearchAutocompleteList
        autocompleteQueryValue={autocompleteQueryValue}
        // ... existing props ...
        autocompleteSubstitutions={autocompleteSubstitutions}
    />

    autocompleteQueryValue={autocompleteQueryValue}
    handleSearch={handleSearchAction}
    searchQueryItem={searchQueryItem}
    onListItemPress={onListItemPress}
    setTextQuery={setTextAndUpdateSelection}
    updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
    ref={listRef}
    personalDetails={personalDetails}
    reports={reports}
    allCards={nonPersonalAndWorkspaceCards}
    allFeeds={allFeeds}
    textInputRef={textInputRef}
    />
    </View>

    autocompleteQueryValue={autocompleteQueryValue}
    handleSearch={handleSearchAction}
    searchQueryItem={searchQueryItem}
    onListItemPress={onListItemPress}
    setTextQuery={setTextAndUpdateSelection}
    updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
    ref={listRef}
    shouldSubscribeToArrowKeyEvents={isAutocompleteListVisible}
    personalDetails={personalDetails}
    reports={reports}
    allCards={nonPersonalAndWorkspaceCards}
    allFeeds={allFeeds}
    textInputRef={textInputRef}
    />
    </View>

    What alternative solutions did you explore? (Optional)

    We can only calculate manually alreadyAutocompletedIDs for some special search key like policy_id, in, ...

  10. melvin-bot commented on Feb 16, 2026

    @melvin-bot

    @aimane-chnaif Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  11. 76 remaining items

  12. added
    ExternalAdded to denote the issue can be worked on by a contributor
    and removed
    ExternalAdded to denote the issue can be worked on by a contributor
    on May 1, 2026
  13. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on May 1, 2026
  14. melvin-bot commented on May 1, 2026

    @melvin-bot

    Current assignee @aimane-chnaif is eligible for the External assigner, not assigning anyone new.

  15. changed the title [-][Due for payment 2026-04-30] Search - The workspace with the same name is not displayed in the autocomplete[/-] [+][$250] [Due for payment 2026-04-30] Search - The workspace with the same name is not displayed in the autocomplete[/+] on May 1, 2026
  16. melvin-bot commented on May 1, 2026

    @melvin-bot
  17. mallenexpensify commented on May 1, 2026

    @mallenexpensify
    Contributor

    @abbasifaizan70 @aimane-chnaif can you please accept the job below? Please reply here and tag me once you have.
    https://www.upwork.com/jobs/~022050309179163591879

    ^ Test case created

  18. abbasifaizan70 commented on May 1, 2026

    @abbasifaizan70
    Contributor

    @mallenexpensify Offer accepted and submitted for payment approval. Thanks

  19. mallenexpensify commented on May 1, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @abbasifaizan70 paid $250 via Upwork
    Contributor+: @aimane-chnaif paid $250 via Upwork

  20. aimane-chnaif commented on May 4, 2026

    @aimane-chnaif
    Contributor

    @mallenexpensify accepted offer

  21. mallenexpensify commented on May 4, 2026

    @mallenexpensify
    Contributor

    @aimane-chnaif paid, summary updated above. Thx.

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributors

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions