Skip to content

Multi-level categories with colons are shown with hierarchical indent…#83200

Merged
yuwenmemon merged 4 commits intoExpensify:mainfrom
ijmalik:82068
Mar 13, 2026
Merged

Multi-level categories with colons are shown with hierarchical indent…#83200
yuwenmemon merged 4 commits intoExpensify:mainfrom
ijmalik:82068

Conversation

@ijmalik
Copy link
Contributor

@ijmalik ijmalik commented Feb 23, 2026

…ation

Explanation of Change

Fixed Issues

$ #82068
PROPOSAL:
$ #82068 (comment)

Tests

  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
  3. 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
Professional Services
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests

QA Steps

Same as Tests

  • 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 image
Android: mWeb Chrome image
iOS: Native image
iOS: mWeb Safari
MacOS: Chrome / Safari image

@melvin-bot
Copy link

melvin-bot bot commented Feb 23, 2026

@ZhenjaHorbach Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@github-actions
Copy link
Contributor

Review: Critical correctness concerns

1. PARENT_CHILD_SEPARATOR change — Likely breaks hierarchy building

Category names from the backend use : (colon-space) as separator (e.g. Food: Meat, Travel: Meals: Breakfast). Changing PARENT_CHILD_SEPARATOR from ": " to ":" produces incorrect splits:

  • "Food: Meat".split(":")["Food", " Meat"] (leading space on child)
  • sortCategories (line 242) uses lodashGet(hierarchy, path) where path comes from this split — hierarchy keys will have leading spaces
  • getCategoryOptionTree (line 66) joins parts back, producing "Food:Meat" instead of "Food: Meat", breaking search matching
  • Display text (line 76) will show " Meat" with a leading space

2. Changing isOneLine from true to false in 4 places

Showing hierarchical indentation in search results may be confusing since filtered results lose parent context.

3. Existing tests will fail

tests/unit/CategoryOptionListUtilsTest.ts has assertions depending on ": " separator format and isOneLine=true for search/selected sections. The PR does not update tests.

Recommendation

If the issue is category names with literal colons being incorrectly treated as hierarchical, the current ": " (colon-space) separator already distinguishes from bare colons. Please run npm run test -- tests/unit/CategoryOptionListUtilsTest.ts to validate.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5b4d64500

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}));

const data = getCategoryOptionTree(searchCategories, true);
const data = getCategoryOptionTree(searchCategories, false);

Choose a reason for hiding this comment

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

P2 Badge Keep parent categories selectable in search results

Switching search results to getCategoryOptionTree(..., false) makes tree-building order-dependent: when a child category is already selected, categoriesForSearch prepends that child before enabledCategories, so getCategoryOptionTree creates a synthetic disabled parent first and then skips the real enabled parent due optionCollection.has(searchText). In large policies (where search is used), this can make a valid parent category unselectable while searching (e.g. selected Food: Meat, search Food).

Useful? React with 👍 / 👎.

},
INDENTS: ' ',
PARENT_CHILD_SEPARATOR: ': ',
PARENT_CHILD_SEPARATOR: ':',

Choose a reason for hiding this comment

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

P2 Badge Preserve trimmed child labels for colon-space categories

Changing PARENT_CHILD_SEPARATOR to ':' causes existing category names in the common "Parent: Child" format to split into segments like ' Child'; since getCategoryOptionTree does not trim segments before rendering, child rows/tooltip text gain a leading space and appear over-indented at each level. This is a visible regression for existing policies that already store names with ": ".

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ijmalik
And actually about this
Did you check this for already-created categories?

Copy link
Contributor Author

@ijmalik ijmalik Mar 11, 2026

Choose a reason for hiding this comment

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

@ZhenjaHorbach Yes, I've already tested this and it's working correctly. My second commit (Multi-level categories - trim leaf) specifically addresses this by trimming the segments before rendering, so existing categories with the ": " format display properly without over-indentation.

@ZhenjaHorbach
Copy link
Contributor

@ijmalik
Please, don't open PRs while you are assigned
Only drafts

@ijmalik ijmalik marked this pull request as draft February 23, 2026 12:45
@ijmalik ijmalik force-pushed the 82068 branch 2 times, most recently from 85a856e to 4a12a98 Compare March 9, 2026 12:49
@ijmalik ijmalik marked this pull request as ready for review March 9, 2026 13:04
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a12a98a72

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow);

const data = getCategoryOptionTree(cutRecentlyUsedCategories, true);
const data = getCategoryOptionTree(cutRecentlyUsedCategories, false);

Choose a reason for hiding this comment

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

P2 Badge Keep recent parent categories selectable

Passing false here enables tree-building for Recent items, but the input order is recency-based, not parent-first. When a child (e.g. Food: Meat) appears before its enabled parent (Food), getCategoryOptionTree() creates a synthetic disabled parent from the child and then drops the real parent on the later pass due optionCollection.has(searchText), leaving a valid recent category unselectable. This regression is user-visible in large workspaces where the Recent section is shown and both parent and child categories were used recently.

Useful? React with 👍 / 👎.


if (selectedOptions.length > 0) {
const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true);
const data = getCategoryOptionTree(selectedOptionsWithDisabledState, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need isOneLine param?
If we use false everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After analyzing the isOneLine usage across the codebase, it appears that it's only used internally within CategoryOptionListUtils and its unit tests. Since we now want hierarchical indentation for all category lists (as per the issue), this parameter seems redundant.

I'll update the PR to:

Remove the isOneLine parameter

Simplify internal calls (no need to pass false)

Update the tests accordingly

This keeps the code cleaner while maintaining the desired behavior. Let me know if you'd like me to proceed with this change.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ijmalik
Are you planning to fix this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ZhenjaHorbach
Yes, I’ll remove the isOneLine parameter and clean up the related code as discussed. I’ll also update the tests accordingly. I’ll push the changes shortly – Let me know if you have any other concerns before I proceed.

Copy link
Contributor

@ZhenjaHorbach ZhenjaHorbach Mar 11, 2026

Choose a reason for hiding this comment

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

Only this remark regarding the code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @ZhenjaHorbach! I've just pushed the changes removing the isOneLine parameter and cleaned up the related code and tests. The PR is now ready for your final review.

Please let me know if you see anything else that needs adjustment.

@ijmalik
Copy link
Contributor Author

ijmalik commented Mar 9, 2026

@ZhenjaHorbach Could you please verify how edge cases like extra spaces before or after the colon are handled in Classic/OldDot and compare them with our PR? We want to ensure our handling remains consistent with existing behavior.

In NewDot, the Category field in expense reports displays extra spaces exactly as entered. OldDot also preserves extra spaces after selection. So this part looks consistent.

See the attached videos for reference:

Classic/OldDot

This PR

.

Additionally, we noticed a separate issue: categories with multiple consecutive spaces

Many: Spaces: After
At https://www.expensify.com/expenses category shown without extra spaces as Many: Spaces: After

(Classic screenshot)

Current behaviour on staging showing truncated category and our PR is also doing same
https://staging.new.expensify.com/search?q=type%3Aexpense%20sortBy%3Adate%20sortOrder%3Adesc

Staging/ThisPR screenshot

This seems like a separate UI display issue (likely unrelated to our changes). Should we handle it in this PR or create a follow‑up issue?

Thanks for your guidance!

@ZhenjaHorbach
Copy link
Contributor

ZhenjaHorbach commented Mar 10, 2026

@ZhenjaHorbach Could you please verify how edge cases like extra spaces before or after the colon are handled in Classic/OldDot and compare them with our PR? We want to ensure our handling remains consistent with existing behavior.

In NewDot, the Category field in expense reports displays extra spaces exactly as entered. OldDot also preserves extra spaces after selection. So this part looks consistent.

See the attached videos for reference:

Classic/OldDot

This PR

.

Additionally, we noticed a separate issue: categories with multiple consecutive spaces

Many: Spaces: After At https://www.expensify.com/expenses category shown without extra spaces as Many: Spaces: After

(Classic screenshot)

Current behaviour on staging showing truncated category and our PR is also doing same https://staging.new.expensify.com/search?q=type%3Aexpense%20sortBy%3Adate%20sortOrder%3Adesc

Staging/ThisPR screenshot

This seems like a separate UI display issue (likely unrelated to our changes). Should we handle it in this PR or create a follow‑up issue?

Thanks for your guidance!

I suppose it's expected
That extra spaces are considered part of a category
As a result, there is an empty space
But this looks strange in OD and ND as for me

@Expensify/design
Could you share your thoughts, please?

@shawnborton
Copy link
Contributor

I think it's fine if we just use consistent behavior with OldDot for the spaces. I would imagine most people won't set up their categories that way?

@dannymcclain
Copy link
Contributor

Agree - I think we should just mirror Classic's behavior for these.

@ijmalik
Copy link
Contributor Author

ijmalik commented Mar 10, 2026

Thanks @ZhenjaHorbach , @shawnborton, and @dannymcclain for the feedback. This PR keeps the behavior consistent with OldDot as discussed. The truncation of categories with multiple spaces appears to be a separate UI issue.

Is there anything else you'd like me to adjust in this PR? Otherwise, I believe it's ready for final review.

@shawnborton
Copy link
Contributor

Sounds good, let's get it into final review.

@ZhenjaHorbach
Copy link
Contributor

ZhenjaHorbach commented Mar 11, 2026

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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
    • 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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • 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
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
2026-03-13.17.21.13.mov
Android: mWeb Chrome
2026-03-13.17.18.00.mov
iOS: HybridApp
2026-03-13.17.21.13.mov
iOS: mWeb Safari
2026-03-13.17.18.00.mov
MacOS: Chrome / Safari
2026-03-13.17.10.26.mov

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/CONST/index.ts 93.54% <ø> (ø)
src/libs/CategoryOptionListUtils.ts 95.50% <100.00%> (+0.82%) ⬆️
... and 38 files with indirect coverage changes

@ZhenjaHorbach
Copy link
Contributor

LGTM!

@melvin-bot melvin-bot bot requested a review from yuwenmemon March 13, 2026 16:25
@yuwenmemon yuwenmemon merged commit 78690d7 into Expensify:main Mar 13, 2026
33 checks passed
@github-actions
Copy link
Contributor

🚧 @yuwenmemon has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify
Copy link
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Contributor

🚀 Deployed to staging by https://github.com/yuwenmemon in version: 9.3.38-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@nlemma
Copy link

nlemma commented Mar 16, 2026

Deploy Blocker #85357 was identified to be related to this PR.

@jponikarchuk
Copy link

Deploy Blocker #85359 was identified to be related to this PR.

@jponikarchuk
Copy link

Deploy Blocker #85361 was identified to be related to this PR.

@jponikarchuk
Copy link

Deploy Blocker #85364 was identified to be related to this PR.

@yuwenmemon
Copy link
Contributor

Revert PR: #85433

@OSBotify
Copy link
Contributor

🚀 Deployed to production by https://github.com/cristipaval in version: 9.3.38-4 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

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.

8 participants