Skip to content

[$250] Multi-level categories/tags with colons are not displayed with hierarchical indentation in certain places in NewDot #82068

Closed

Description

@yuwenmemon

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: N/A
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A
Email or phone of affected tester (no customers): N/A
Logs: N/A
Expensify/Expensify Issue URL: N/A
Issue reported by: @brianlee-expensify
Slack conversation: https://expensify.slack.com/archives/C05LX9D6E07/p1770752446067979

Action Performed:

  1. Create or use a workspace with multi-level categories that use colons as separators (e.g. Primary:Secondary:Tertiary)
  2. Open an expense and view the category selector in NewDot

Expected Result:

Multi-level categories should be displayed with hierarchical indentation (like in Classic/OldDot), where sub-categories are visually nested under their parent categories. For example:

Primary
  Secondary
    Tertiary
Professional Services

Actual Result:

Categories are displayed as flat, colon-separated strings (e.g. Primary:Secondary:Tertiary), making them hard to read — especially for long or deeply nested category names. The full text is also difficult to read at the individual expense level because the display area is too narrow for long concatenated names.

In Classic/OldDot, the dropdown properly parses the colons and renders sub-categories with indentation, which is significantly more readable.

Workaround:

Users can hover over each option in NewDot to see the full text, but this is far less efficient than the Classic hierarchical display.

Platforms:

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

Screenshots/Videos

NewDot (current — flat colon-separated display):
Long category names like General and Administrative Expenses/Other Ge... are truncated and hard to read.
Image

Classic/OldDot (expected — hierarchical indentation):
Categories with colons are parsed and displayed with visual nesting:

  • Primary shown at root level
  • Secondary indented under Primary
  • Tertiary further indented under Secondary
Image

cc @JmillsExpensify @puneetlath @dannymcclain @shawnborton


View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @yuwenmemon
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022021404126598547656
  • Upwork Job ID: 2021404126598547656
  • Last Price Increase: 2026-02-18
  • Automatic offers:
    • ijmalik | Contributor | 110649720

Activity

  1. added
    InternalRequires API changes or must be handled by Expensify staff
    BugSomething is broken. Auto assigns a BugZero manager.
    on Feb 11, 2026
  2. melvin-bot commented on Feb 11, 2026

    @melvin-bot

    Triggered auto assignment to @heyjennahay (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.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    and removed
    InternalRequires API changes or must be handled by Expensify staff
    on Feb 11, 2026
  4. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Feb 11, 2026
  5. melvin-bot commented on Feb 11, 2026

    @melvin-bot

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

  6. changed the title [-]Multi-level categories/tags with colons are not displayed with hierarchical indentation in NewDot[/-] [+][$250] Multi-level categories/tags with colons are not displayed with hierarchical indentation in NewDot[/+] on Feb 11, 2026
  7. melvin-bot commented on Feb 11, 2026

    @melvin-bot
  8. imgyf commented on Feb 11, 2026

    @imgyf
    Contributor

    Proposal

    Please re-state the problem that we are solving in this issue in your own words.

    When multi-level categories use colons as separators (e.g., Primary:Secondary:Tertiary), they are displayed as flat concatenated strings in NewDot's category selector. This makes deeply nested categories hard to read, especially when names are long and get truncated. Classic/OldDot correctly parses the colons and displays sub-categories with visual indentation.

    What is the root cause of that problem?

    The category selector renders category names as-is without parsing the colon delimiter to determine hierarchy depth. There's no logic to split on : and apply indentation styling based on the resulting segments.

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

    1. In the category option rendering logic (likely in src/components/OptionsList or the category-specific selector), split the category name by : to determine the hierarchy level
    2. Calculate indentation depth based on the number of segments (e.g., Primary:Secondary = depth 2)
    3. Display only the final segment as the visible text, with left padding proportional to depth
    4. Ensure the full path is still used as the value/key for selection

    For example:

    Primary              → depth 0, no indent
      Secondary          → depth 1, 16px indent  
        Tertiary         → depth 2, 32px indent
    

    What alternative solutions did you explore? (Optional)

    • Tooltip-only approach: Keep flat display but show full hierarchy in tooltip. Rejected because it doesn't match Classic behavior and is less efficient for users.
    • Tree component: Build a collapsible tree structure. Rejected as overkill — simple indentation matches Classic behavior.
  9. mukhrr commented on Feb 11, 2026

    @mukhrr
    Contributor

    Proposal

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

    Multi-level categories/tags with colons are not displayed with hierarchical indentation - flat text without tree-like shape

    What is the root cause of that problem?

    The PARENT_CHILD_SEPARATOR const is defined as : (colon with a trailing space) at src/CONST/index.ts.

    PARENT_CHILD_SEPARATOR: ': ',

    The getCategoryOptionTree() function in src/libs/CategoryOptionListUtils.ts splits category names using this separator:

    const array = option.name.split(CONST.PARENT_CHILD_SEPARATOR);

    When categories come from accounting integrations (e.g., QuickBooks), they use : without a space — e.g., "Primary:Secondary:Tertiary". Since "Primary:Secondary:Tertiary".split(': ') produces ["Primary:Secondary:Tertiary"] (a single-element array), no hierarchy is detected and no indentation is applied. The same issue affects the sortCategories() function which also splits by ': '.

    for (const category of sortedCategories) {
    const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR);

    OldDot/Classic handles this correctly because it parses : as the separator regardless of spacing.

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

    We need to replace the literal CONST.PARENT_CHILD_SEPARATOR split with a regex /\s*:\s*/ that handles both : and : and add a splitCategoryName(name) helper that splits by /\s*:\s*/ and provides a getSearchText(index, isLeaf) method. For leaf entries it returns the original option.name (preserving the backend key for matching). For parent entries it finds the Nth colon position in the original string and returns substring(0, colonPos).trim(), preserving the original separator style in the key.

    -const array = option.name.split(CONST.PARENT_CHILD_SEPARATOR);
    +const {parts, getSearchText} = splitCategoryName(option.name);
    function getCategoryOptionTree(options: Record<string, Category> | Category[], isOneLine = false, selectedOptions: Category[] = []): OptionTree[] {
        // existing logic ...
    
            const {parts, getSearchText} = splitCategoryName(option.name);
    
            for (let index = 0; index < parts.length; index++) {
                const optionName = parts.at(index);
                if (!optionName) {
                    continue;
                }
    
                const indents = times(index, () => CONST.INDENTS).join('');
                const isChild = parts.length - 1 === index;
                const searchText = getSearchText(index, isChild);
                const selectedParentOption = !isChild && Object.values(selectedOptions).find((op) => op.name === searchText);
                const isParentOptionDisabled = !selectedParentOption || !selectedParentOption.enabled || selectedParentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
    
               // existing logic ... 
    
        return Array.from(optionCollection.values());
    }

    Also in sortCategories:

    -const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR)
    +const path = category.name.split(/\s*:\s*/);

    What alternative solutions did you explore? (Optional)

  10. HiepPP commented on Feb 11, 2026

    @HiepPP

    Proposal for Issue #82068

    Problem

    Multi-level categories that use colons as separators (e.g., Primary:Secondary:Tertiary) are displayed as flat, colon-separated strings in the category picker instead of with hierarchical indentation. This affects:

    1. Recently used categories
    2. Selected categories
    3. Search results

    The "All" categories section correctly displays hierarchical indentation.

    Root Cause

    The issue is in src/libs/CategoryOptionListUtils.ts. The getCategoryOptionTree() function has an isOneLine parameter that controls whether to build the tree hierarchy with indentation:

    • When isOneLine = true: The function skips tree building and returns the full category name without indentation
    • When isOneLine = false: The function properly parses the colon separator and builds indented display names

    Currently, getCategoryListSections() passes isOneLine = true for:

    • Selected categories (lines 127, 158)
    • Search results (line 146)
    • Recent categories (line 195)

    Only the "All" section (lines 171, 204) correctly passes isOneLine = false.

    Root Cause Verification Checklist

    • This is the actual root cause, not a symptom - The isOneLine parameter directly controls whether indentation is applied
    • Compared with working similar flows - The "All" section works correctly because it passes isOneLine = false
    • All call sites of affected functions considered - All four calls to getCategoryOptionTree() in getCategoryListSections() are affected
    • For detection logic: function receives complete data - Yes, the data is complete but the flag prevents processing

    Minimal Fix Checklist

    • Fix is at the earliest point of failure (not a workaround) - Yes, the fix is in the data preparation function
    • No new parameters added to existing functions - Yes, just changing boolean values
    • No new utility functions created - Yes
    • No refactoring of unrelated code - Yes
    • Change follows existing patterns exactly - Yes, follows the pattern used by the "All" section
    • Line count is minimized (prefer 1-line fixes over 10-line fixes) - Yes, 4 lines changed

    Working vs Broken Comparison

    Aspect Working Flow ("All" section) Broken Flow (Selected/Recent/Search)
    Flow name All categories Selected, Recent, Search
    Key condition isOneLine = false isOneLine = true
    Data passed Full categories with tree building Full categories without tree building
    Result Hierarchical indentation Flat colon-separated strings

    Alternative Solutions Considered

    Alternative 1: Modify getCategoryOptionTree() to always build tree

    Remove the isOneLine parameter entirely and always build the tree hierarchy.

    Why rejected: The isOneLine parameter exists for a reason - when searching, users want to see the full category path to distinguish between similarly named subcategories. Changing this behavior globally could break other use cases.

    Alternative 2: Add new parameter for indentation only

    Add a new parameter like shouldIndent that controls indentation separately from tree building.

    Why rejected: This adds complexity without benefit. The existing isOneLine flag is meant to control both tree building and indentation display. The simpler fix is to pass false where we want indentation.

    Alternative 3: Post-process in RadioListItem

    Handle indentation in the UI component instead of the data layer.

    Why rejected: This would require passing additional metadata to the component and would be a more invasive change. The current approach of preparing data in CategoryOptionListUtils is the established pattern.

    Solution

    Change isOneLine from true to false for the three broken flows in getCategoryListSections():

    1. Line 127: Selected categories when no enabled categories
    2. Line 146: Search results
    3. Line 158: Selected categories section
    4. Line 195: Recent categories section

    This ensures all category displays use the same hierarchical indentation logic.

    Implementation

    Files Modified

    • src/libs/CategoryOptionListUtils.ts

    Key Changes

    // Line 127 - Change isOneLine from true to false
    const data = getCategoryOptionTree(selectedOptionsWithDisabledState, false);
    
    // Line 146 - Change isOneLine from true to false
    const data = getCategoryOptionTree(searchCategories, false);
    
    // Line 158 - Change isOneLine from true to false
    const data = getCategoryOptionTree(selectedOptionsWithDisabledState, false);
    
    // Line 195 - Change isOneLine from true to false
    const data = getCategoryOptionTree(cutRecentlyUsedCategories, false);

    Testing Approach

    1. Create a workspace with multi-level categories using colons (e.g., Primary:Secondary:Tertiary)
    2. Create an expense and open the category picker
    3. Verify that:
      • The "All" section shows hierarchical indentation (already works)
      • The "Recent" section shows hierarchical indentation (fixed)
      • The "Selected" section shows hierarchical indentation (fixed)
      • Search results show hierarchical indentation (fixed)
    4. Verify that selecting a subcategory still works correctly
    5. Verify that the search functionality still filters correctly

    Edge Cases

    • Empty/null states: The function already handles empty arrays correctly
    • Single-level categories: Will display the same as before (no indentation needed)
    • Deep nesting (3+ levels): Will display with appropriate indentation levels
    • Offline/online behavior: No impact, this is a display-only change
    • Different report types: No impact, this affects the category picker component used across all flows

    Performance Impact

    Minimal. The tree building logic is already used for the "All" section and performs well. The additional processing for selected/recent categories (typically small sets) will have negligible impact.

  11. 72 remaining items

  12. brianlee-expensify commented on Jun 4, 2026

    @brianlee-expensify
    Contributor

    friendly bump for @yuwenmemon !

  13. ijmalik commented on Jun 9, 2026

    @ijmalik
    Contributor

    Another friendly bump for @yuwenmemon and @ZhenjaHorbach when you get a quick moment to check the payment tracker issue. Appreciate it!

    CC: @brianlee-expensify

  14. melvin-bot commented on Jun 12, 2026

    @melvin-bot

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

  15. ZhenjaHorbach commented on Jun 15, 2026

    @ZhenjaHorbach
    Contributor

    BugZero Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

    It's a new feature

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: NA

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    Regression Test Proposal

    Test:

    1. Open App
    2. Create or use a workspace with multi-level categories that use colons as separators (e.g. Primary:Secondary:Tertiary)
    3. Open an expense and view the category selector in NewDot
    4. Verify that Multi-level categories will be displayed with hierarchical indentation (like in Classic/OldDot), where sub-categories are visually nested under their parent categories. For example:
    Primary
      Secondary
        Tertiary
    

    Do we agree 👍 or 👎

    Zapier Logs Run ID: 00040eee-9b84-a64e-4e09-50f095b761f4
  16. ZhenjaHorbach commented on Jun 15, 2026

    @ZhenjaHorbach
    Contributor

    Oh
    Sorry
    I missed this issue
    And about payment
    We had a few regressions
    But this issue was quite complex, with a lot of edge cases
    Plus, we expanded the scope of the issue, which required significant time for testing
    #85448 (comment)
    So, if you don't mind, I propose 375$ here
    CC: @trjExpensify(Since you proposed updating the UI in a similar way to the OD 😁)

  17. trjExpensify commented on Jun 16, 2026

    @trjExpensify
    Contributor

    Works for me. 👍

  18. ZhenjaHorbach commented on Jun 16, 2026

    @ZhenjaHorbach
    Contributor

    @mallenexpensify
    We are done here!

  19. mallenexpensify commented on Jun 16, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @ijmalik paid $375 via Upwork
    Contributor+: @ZhenjaHorbach due $375 via NewDot

    ^ Test case created. Thx

  20. garrettmknight commented on Jun 18, 2026

    @garrettmknight
    Contributor

    $375 approved for @ZhenjaHorbach

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

Metadata

Metadata

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

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions