Display full category hierarchy across category display surfaces - #95830
Display full category hierarchy across category display surfaces#95830KJ21-ENG wants to merge 10 commits into
Conversation
Replace the leaf-only getDecodedLeafCategoryName with a full-path getDecodedFullCategoryName (cleaned segments rejoined with ': ') and route every category display site through it — report table cell + group header, report details, chat transaction preview, money request confirmation, the split flow, and merge/duplicate review — so the full 'Parent: Child' path is shown consistently, matching Classic Expensify. Display-only; raw picker, grouping-key, and merge-conflict values are unchanged. Fixes Expensify#94710
…gory-hierarchy-fix
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65a590cf13
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return Str.htmlDecode(leaf.trim()); | ||
| function getDecodedFullCategoryName(categoryName: string): string { | ||
| const segments = processCategoryNameSegments(categoryName).map((segment) => segment.trim()); | ||
| return Str.htmlDecode(segments.join(`${CONST.PARENT_CHILD_SEPARATOR} `)); |
There was a problem hiding this comment.
Preserve distinct category names when rendering paths
When a workspace contains categories that differ only by separator spacing, e.g. A:B and A: B (the category form only rejects exact duplicate keys), this formatter renders both as A: B. The report rows/grouping and saves still use the raw category value, so users can end up with two different categories/groups that are visually indistinguishable on the updated display surfaces. Either preserve the stored separator spacing for display or normalize category names before allowing both variants.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is intentional behaviour.
cc: @dmkt9
There was a problem hiding this comment.
Yes, I was aware of this issue when reviewing the proposal. I think it's acceptable because it's rare for a user to have both A:B and A: B as category names. However, I think this should be confirmed from a product perspective.
Could you add a few screenshots to illustrate this scenario and tag heyjennahay and johncschuster for confirmation?
There was a problem hiding this comment.
@dmkt9 @heyjennahay @johncschuster
Screen.Recording.2026-07-16.at.1.01.30.AM-compressed.mp4
There was a problem hiding this comment.
Personally I think this is fine and an unlikely to occur edge case. If a customer makes two incredibly similar tags I don't think it should be our responsibility to differentiate them. We will display them as set up and if they don't like it they can always change the tag.
|
@KJ21-ENG you have conflicts |
…gory-hierarchy-fix
|
@dmkt9 Fixed! |
…gory-hierarchy-fix
|
@dmkt9 kind ping here. Thanks! |
@KJ21-ENG Hmm. I don't think the right thing to do here is to ping me. You first need to get confirmation from a production perspective here: #95830 (comment) before I can review it. Also, I don't understand why, when you're asking someone for their opinion, you don't include any context or explain what exactly you want them to confirm. No one is going to read through the entire discussion just to figure out what you're asking. |
|
Actually, I tagged you and couple of produt members in inline review itself, so I thought it would be enough context to take deccesion and more over there are not many messages in this PR thread there is only one and that too from your review. The reason that I pingged you is very simple that you can contact product members in internal slack as from my side i already did here in this PR thread. and I dont think anything is wrong it it. Yaa, but I agree that it makes more sense to ping product members again along with you if not beofore. |
|
@codex review |
|
@KJ21-ENG Please merge the latest main |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
|
@MelvinBot review |
ReviewOverall this is clean and well-scoped — a mechanical rename of The one open design question is now resolved. The What I verified
Minor (non-blocking), your call whether to note: the category pickers, Search filter, and Workspace → Categories settings still render the raw stored name via LGTM. Since the product confirmation you were blocked on has landed, this looks ready for your checklist review. |
…gory-hierarchy-fix
|
@dmkt9 opps, i thought i did back then. NW, doing it now. Applogies |
…gory-hierarchy-fix
Reviewer Checklist
Screenshots/VideosMacOS: Chrome / Safarimac.safari.mp4 |
|
@KJ21-ENG You have some conflicts to resolve. |
…gory-hierarchy-fix # Conflicts: # src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx
Fixed ! |
…gory-hierarchy-fix # Conflicts: # src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx




Explanation of Change
Hierarchical categories such as
Meals and Entertainment: Otherare stored in full ontransaction.category, but every main display surface formatted them withgetDecodedLeafCategoryName(), which deliberately keeps only the last segment — so the app rendered justOther, diverging from Classic.Since the fix is universal (per the reporter's and C+'s confirmation in the issue), no surface keeps the leaf-only display. This PR therefore changes the one shared formatter instead of adding a parallel helper:
getDecodedLeafCategoryName()insrc/libs/CategoryUtils.tsis renamed togetDecodedFullCategoryName(). It reuses the existingprocessCategoryNameSegments()cleanup (drops empty/stray segments, keeps a single trailing colon), trims each segment, and joins them with:, so output is always the readableParent: Childform —A:BandA: Bboth render asA: B,Food: : Meatrenders asFood: Meat.CategoryCell), the narrow transaction row (TransactionItemRow— switched from the baregetDecodedCategoryName()so wide and narrow render identically), the group-by-category header (MoneyRequestReportGroupHeader), the expense details view (MoneyRequestView), the chat transaction preview (TransactionPreviewContent), the money request confirmation field (CategoryField), the split flow (SplitListItemvisible text + accessibility label,SplitExpenseEditPage), and merge/duplicate review (MergeTransactionUtils.getDisplayValue).keyForList, and merge conflict detection (getMergeFieldValue) still use the raw stored category, theReportLayoutUtilsgrouping key still uses its existing decoded value, and tag formatting (getCommaSeparatedTagNameWithSanitizedColons) is unchanged — so grouping, selection, saving, and merge logic are unchanged. Display-only; no API/Onyx changes.Tests: the
CategoryUtilsunit tests are reworked for the new helper (full path, separator-spacing normalization, empty middle segments, trailing colons, single segment, empty input), plus new render tests asserting the full hierarchy in the wide/narrow transaction row and in the category group header (with tag groups still using the tag formatter).Fixed Issues
$ #94710
PROPOSAL: #94710 (comment)
Tests
Setup: on a workspace, enable categories and create a hierarchical category by naming it
Meals and Entertainment: Other(Workspace settings → Categories → Add category).Meals and Entertainment: Other.Meals and Entertainment: Other(not justOther).Meals and Entertainment: Other. Verify the same in the Search page's Expenses table (wide) — it renders the same category cell.<date> • Meals and Entertainment: Other).Meals and Entertainment: Other.A:B(no space) and verify the surfaces from steps 2–9 display it asA: B(the category pickers, Search results table, and workspace Categories settings intentionally keep showing the stored name).Food: : Meat) and verify every surface above still renders cleanly asFood: Meat— no crash, no empty segment, and the raw stored value (visible when reopening the category picker) is unchanged.Offline tests
Category display is read-only formatting of data already stored locally in Onyx, so there is no offline-specific code path.
Meals and Entertainment: Other.QA Steps
Same as the Tests section (all steps can be run on staging with any workspace admin account).
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Screen.Recording.2026-07-11.at.3.55.33.AM.mov
Android: mWeb Chrome
Record_2026-07-11-03-11-29.mp4
iOS: Native
Screen.Recording.2026-07-11.at.2.51.56.AM.mov
iOS: mWeb Safari
Screen.Recording.2026-07-11.at.3.12.35.AM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-11.at.2.46.55.AM.mov