Skip to content

Display full category hierarchy across category display surfaces - #95830

Open
KJ21-ENG wants to merge 10 commits into
Expensify:mainfrom
KJ21-ENG:KJ21-ENG/94710-category-hierarchy-fix
Open

Display full category hierarchy across category display surfaces#95830
KJ21-ENG wants to merge 10 commits into
Expensify:mainfrom
KJ21-ENG:KJ21-ENG/94710-category-hierarchy-fix

Conversation

@KJ21-ENG

@KJ21-ENG KJ21-ENG commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Hierarchical categories such as Meals and Entertainment: Other are stored in full on transaction.category, but every main display surface formatted them with getDecodedLeafCategoryName(), which deliberately keeps only the last segment — so the app rendered just Other, 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() in src/libs/CategoryUtils.ts is renamed to getDecodedFullCategoryName(). It reuses the existing processCategoryNameSegments() cleanup (drops empty/stray segments, keeps a single trailing colon), trims each segment, and joins them with : , so output is always the readable Parent: Child form — A:B and A: B both render as A: B, Food: : Meat renders as Food: Meat.
  • All category display call sites now use it: the report table category cell (CategoryCell), the narrow transaction row (TransactionItemRow — switched from the bare getDecodedCategoryName() 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 (SplitListItem visible text + accessibility label, SplitExpenseEditPage), and merge/duplicate review (MergeTransactionUtils.getDisplayValue).
  • Behavior-bearing values are untouched: the category cell's picker comparison value, keyForList, and merge conflict detection (getMergeFieldValue) still use the raw stored category, the ReportLayoutUtils grouping 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 CategoryUtils unit 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

  • Verify that no errors appear in the JS console

Setup: on a workspace, enable categories and create a hierarchical category by naming it Meals and Entertainment: Other (Workspace settings → Categories → Add category).

  1. Submit an expense to the workspace and pick the category Meals and Entertainment: Other.
  2. On the money request confirmation page, verify the Category field shows Meals and Entertainment: Other (not just Other).
  3. Open the expense report on a wide screen and verify the Category column in the transaction table shows the full Meals and Entertainment: Other. Verify the same in the Search page's Expenses table (wide) — it renders the same category cell.
  4. Narrow the window (or use mobile) and verify the transaction row shows the full category after the date (<date> • Meals and Entertainment: Other).
  5. In the report, set the layout to group by Category and verify the group header shows the full Meals and Entertainment: Other.
  6. Open the expense (details view) and verify the Category row shows the full hierarchy.
  7. Go to the workspace chat and verify the expense/transaction preview shows the full hierarchy next to the folder icon.
  8. Split the expense (Split expense flow) and verify each split row shows the full hierarchy, and that opening a split to edit shows the full hierarchy in its Category field.
  9. Create a duplicate of the categorized expense, start the merge/duplicate review flow, and verify the category values offered for review show the full hierarchy.
  10. Rename the category to A:B (no space) and verify the surfaces from steps 2–9 display it as A: B (the category pickers, Search results table, and workspace Categories settings intentionally keep showing the stored name).
  11. If the workspace uses multi-level tags, set the report layout to group by Tag and verify tag group headers are still formatted as before (comma-separated tag levels).
  12. Failure/edge case: rename the category to a messy name with stray colons (e.g. Food: : Meat) and verify every surface above still renders cleanly as Food: 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.

  1. Complete Tests steps 1 and 5 while online (submit the categorized expense and set the report layout to group by Category), then go offline.
  2. Open the expense report and verify the transaction table, group header, and details view still show the full Meals and Entertainment: Other.
  3. Verify that no errors appear in the JS console.

QA Steps

  • Verify that no errors appear in the JS console

Same as the Tests section (all steps can be run on staging with any workspace admin account).

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 followed proper code patterns (see Reviewing the code)
    • 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 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)
  • 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)
  • 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.
  • 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
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

KJ21-ENG added 3 commits July 9, 2026 17:11
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
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
...RequestConfirmationList/sections/CategoryField.tsx 81.25% <100.00%> (ø)
...equestReportView/MoneyRequestReportGroupHeader.tsx 100.00% <100.00%> (+100.00%) ⬆️
...c/components/ReportActionItem/MoneyRequestView.tsx 74.88% <100.00%> (ø)
...ents/TransactionItemRow/DataCells/CategoryCell.tsx 100.00% <100.00%> (ø)
src/components/TransactionItemRow/index.tsx 95.16% <100.00%> (+11.29%) ⬆️
src/libs/CategoryUtils.ts 74.31% <100.00%> (-0.24%) ⬇️
src/libs/MergeTransactionUtils.ts 79.65% <100.00%> (ø)
...m/TransactionPreview/TransactionPreviewContent.tsx 77.34% <0.00%> (ø)
...omponents/SelectionList/ListItem/SplitListItem.tsx 0.00% <0.00%> (ø)
src/pages/iou/SplitExpenseEditPage.tsx 0.00% <0.00%> (ø)
... and 8 files with indirect coverage changes

@KJ21-ENG
KJ21-ENG marked this pull request as ready for review July 10, 2026 22:03
@KJ21-ENG
KJ21-ENG requested review from a team as code owners July 10, 2026 22:03
@melvin-bot
melvin-bot Bot requested review from dmkt9 and heyjennahay and removed request for a team July 10, 2026 22:03
@melvin-bot

melvin-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

@dmkt9 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]

@melvin-bot
melvin-bot Bot removed the request for review from a team July 10, 2026 22:03

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

Copy link
Copy Markdown

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: 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".

Comment thread src/libs/CategoryUtils.ts
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} `));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@KJ21-ENG KJ21-ENG Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional behaviour.

cc: @dmkt9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dmkt9 @heyjennahay @johncschuster

Screen.Recording.2026-07-16.at.1.01.30.AM-compressed.mp4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@KJ21-ENG

This is intentional behaviour.

cc: @dmkt9

I'm sorry, I'm not following exactly what is intentional behavior. The two near-identical categories being indistinguishable from each other?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@dmkt9

dmkt9 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@KJ21-ENG you have conflicts

@KJ21-ENG

Copy link
Copy Markdown
Contributor Author

@dmkt9 Fixed!

@KJ21-ENG

Copy link
Copy Markdown
Contributor Author

@dmkt9 kind ping here. Thanks!

@dmkt9

dmkt9 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@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.

@KJ21-ENG

Copy link
Copy Markdown
Contributor Author

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.

cc: @dmkt9 @heyjennahay @johncschuster

heyjennahay
heyjennahay previously approved these changes Jul 29, 2026

@heyjennahay heyjennahay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Product change LGTM

@dmkt9

dmkt9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@dmkt9

dmkt9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@KJ21-ENG Please merge the latest main

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 708cecf2b8

ℹ️ 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".

@dmkt9

dmkt9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

Overall this is clean and well-scoped — a mechanical rename of getDecodedLeafCategoryNamegetDecodedFullCategoryName plus routing every leaf-only display surface through it. I verified the key correctness invariants and nothing blocks from a code perspective.

The one open design question is now resolved. The A:B vs A: B indistinguishability that Codex flagged (and that you were waiting on product for) was confirmed acceptable by both heyjennahay ("this is fine and an unlikely to occur edge case") and johncschuster. So that's cleared.

What I verified
  • Behavior-bearing values are untouched (the important part). Only display goes through the new formatter; the raw stored value is preserved everywhere it matters:
    • Merge conflict detection uses getMergeFieldValue (raw); only getDisplayValue decodes for the review UI.
    • Picker comparison keeps the raw name in CategoryCell (categoryForComparison).
    • The group-by-category grouping key in ReportLayoutUtils still uses getDecodedCategoryName (unchanged), and tag headers still use getCommaSeparatedTagNameWithSanitizedColons.
  • No leaf-only surface was missedgetDecodedLeafCategoryName has zero remaining references, and all 9 call sites now use the full-path helper.
  • Narrow row is a slightly bigger change than the leaf fix. TransactionItemRow switched from the bare getDecodedCategoryName to getDecodedFullCategoryName, which is correct (it makes narrow match wide) — just note it now also normalizes separator spacing and drops stray segments in the narrow layout, not only the leaf. Intended per the PR description.
  • The reworked unit tests + the two new render tests cover the full path, spacing normalization, empty middle segments, trailing colons, single-segment, empty input, and HTML entities.

Minor (non-blocking), your call whether to note: the category pickers, Search filter, and Workspace → Categories settings still render the raw stored name via getDecodedCategoryName (no : normalization), so a A:B category shows as A:B there but A: B on the display surfaces. The PR calls this out as intentional and it's consistent with keeping picker/comparison values raw, so I'd leave it — just flagging it's a deliberate inconsistency rather than an oversight.

LGTM. Since the product confirmation you were blocked on has landed, this looks ready for your checklist review.

@dmkt9

dmkt9 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@KJ21-ENG Please merge the latest main

@KJ21-ENG bump

@KJ21-ENG

KJ21-ENG commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@dmkt9 opps, i thought i did back then. NW, doing it now. Applogies

@dmkt9

dmkt9 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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 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 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)
  • 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)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • 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
    • 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 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.
  • 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 android hybrid
Android: mWeb Chrome android chrome
iOS: HybridApp ios hybrid
iOS: mWeb Safari ios safari
MacOS: Chrome / Safari
mac.safari.mp4

@dmkt9

dmkt9 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@KJ21-ENG You have some conflicts to resolve.

@dmkt9 dmkt9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@melvin-bot
melvin-bot Bot requested a review from carlosmiceli August 4, 2026 14:49
…gory-hierarchy-fix

# Conflicts:
#	src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx
@KJ21-ENG

KJ21-ENG commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@KJ21-ENG You have some conflicts to resolve.

Fixed !

…gory-hierarchy-fix

# Conflicts:
#	src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx
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.

5 participants