Skip to content

feat: [CFI] Add group-by report layout feature#72943

Merged
nkuoch merged 12 commits into
Expensify:mainfrom
TaduJR:feat-CFI-Add-group-by-report-layout-feature
Nov 25, 2025
Merged

feat: [CFI] Add group-by report layout feature#72943
nkuoch merged 12 commits into
Expensify:mainfrom
TaduJR:feat-CFI-Add-group-by-report-layout-feature

Conversation

@TaduJR

@TaduJR TaduJR commented Oct 20, 2025

Copy link
Copy Markdown
Contributor

Explanation of Change

Explanation of Change

This PR implements a group-by report layout feature that allows users to organize expense report transactions by Category or Tag, matching the familiar OldDot functionality that NewDot customers have been requesting.

Key Changes:

  1. Data Layer & Backend Integration
  • Added NVP_REPORT_LAYOUT_GROUP_BY Onyx key for storing user preference
  • Reuses existing OldDot NVP key (expensify_groupByOption) for backward compatibility
  • No new backend API needed - uses existing SetNameValuePair command
  • Values: 'mcc' (Category - default) or 'tag' (Tag)
  1. Action Handler with Pattern A Offline UX
  • Created src/libs/actions/ReportLayout.ts with optimistic updates
  • Instant visual feedback via transaction regrouping (no pending/error UI needed)
  • Automatic rollback on API failure
  • Works seamlessly offline - changes queued and synced when connection restores
  1. Business Logic - Transaction Grouping
  • Created src/libs/ReportLayoutUtils.ts with grouping
  • Groups transactions by category or tag with accurate subtotals
  • Sorts groups by order on the category/tag values alphabetically (A>Z)
  • Handles edge cases: uncategorized expenses, missing tags, multi-currency reports
  • Fully internationalized with translateLocal() for all user-facing text
  1. UI Components
  • MoneyRequestReportGroupHeader (src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx):
    • Inline amount formatting: "Category - $15.00"
    • Always-visible checkboxes for bulk group selection
    • Responsive: 12px padding (desktop), 16px horizontal + 44px min height (mobile)
    • Pixel-perfect 16px font size matching Figma specs
  • ReportLayoutPage (src/pages/settings/Report/ReportLayoutPage.tsx):
    • Settings page with "Group by:" label and radio selection
    • Category and Tag options (no "None" option per team decision)
    • Opens in RHP (Right Hand Panel) modal
  1. Navigation & Routing
  • Deep link support: r/:reportID/settings/report-layout
  • Route registered in modal stack navigator
  • Uses Navigation.dismissModal() (modern pattern, not deprecated goBack())
  1. Menu Integration
  • Added "Report layout" option to three-dots menu (MoneyReportHeader)
  • Menu item only shows for expense reports with 2+ transactions
  • Uses Feed icon (stacked layers visual)
  • Positioned logically with other report customization options
  1. Grouped Transaction Display
  • Modified MoneyRequestReportTransactionList.tsx to render transactions in groups
  • Groups always expanded (non-collapsible)
  • 8px spacing: between header and first transaction, between each transaction, between groups
  • Bulk selection at group level: checkbox on header selects all transactions in group
  • Indeterminate checkbox state when some (not all) transactions selected
  • Instant regrouping when preference changes
  1. Internationalization
  • Added translations for all 10 supported languages (en, es, de, fr, it, ja, nl, pl, pt-BR, zh-hans)
  • Translation keys: reportLayout.reportLayout, reportLayout.groupByLabel, reportLayout.groupBy.category, reportLayout.groupBy.tag, reportLayout.selectGroup

Fixed Issues

$ #72100
PROPOSAL: #72100 (comment)

Tests

Desktop Testing

Category Grouping:

  1. Open an expense report with 5+ transactions across multiple categories (e.g., 2 "Gas" transactions, 3 "Office Supplies" transactions)
  2. Click More button
  3. Click "Report layout" in the menu
  4. Verify RHP opens with header "Report layout" and "Group by:" label
  5. Verify "Category" is selected by default (green checkmark)
  6. Verify transactions are grouped by category with group headers showing "Category Name - $Amount"
  7. Verify groups are sorted order on the category/tag values alphabetically (A>Z)
  8. Verify each group header has a checkbox
  9. Click a group header checkbox
  10. Verify all transactions in that group are selected
  11. Click individual transaction to deselect
  12. Verify group checkbox shows indeterminate state

Tag Grouping:

  1. With report still open, click More → "Report layout"
  2. Select "Tag" option
  3. Verify modal dismisses
  4. Verify transactions instantly regroup by tag
  5. Verify group headers now show tag names with subtotals
  6. Verify untagged transactions show in separate group

Preference Persistence:

  1. Navigate to a different expense report
  2. Verify tag grouping is still active (preference persisted)
  3. Change back to category grouping
  4. Open a third report
  5. Verify category grouping is active

Offline Mode:

  1. Enable airplane mode (disconnect internet)
  2. Change grouping preference from category to tag
  3. Verify transactions regroup instantly (optimistic update)
  4. Disable airplane mode (reconnect internet)
  5. Wait ~5 seconds
  6. Verify preference remains as tag (synced to server)

Edge Cases:

  1. Open a report with only 1 transaction
  2. Verify "Report layout" option does NOT appear in More menu
  3. Open a report where all transactions have the same category
  4. Verify only one group appears
  5. Open a report with 0 transactions
  6. Verify "Report layout" option does NOT appear in three-dots menu

Mobile Testing (iOS/Android)

Category Grouping:

  1. Open an expense report with 5+ transactions on mobile device
  2. Tap More menu
  3. Verify "Report layout" option appears
  4. Tap "Report layout"
  5. Verify RHP opens with "Report layout" header and back button
  6. Verify "Group by:" label is visible
  7. Verify "Category" is selected by default
  8. Tap "Tag" option
  9. Verify modal dismisses and transactions regroup by tag
  10. Verify checkboxes are visible on group headers

Group Selection:

  1. Tap a group header checkbox
  2. Verify all transactions in that group are selected
  3. Verify group checkbox shows checked state
  4. Tap individual transaction checkbox to deselect one
  5. Verify group checkbox shows indeterminate state
  6. Tap group checkbox again
  7. Verify all transactions in group are deselected
  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."
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
    • MacOS: Desktop
  • 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 Screenshot 2025-11-22 at 3 42 14 in the afternoon Screenshot 2025-11-22 at 3 42 06 in the afternoon Screenshot 2025-11-22 at 3 41 45 in the afternoon Screenshot 2025-11-22 at 3 41 36 in the afternoon Screenshot 2025-11-22 at 3 41 30 in the afternoon Screenshot 2025-11-22 at 3 41 25 in the afternoon Screenshot 2025-11-22 at 3 41 18 in the afternoon
Android: mWeb Chrome Screenshot 2025-11-22 at 3 37 59 in the afternoon Screenshot 2025-11-22 at 3 37 53 in the afternoon Screenshot 2025-11-22 at 3 37 44 in the afternoon Screenshot 2025-11-22 at 3 37 37 in the afternoon Screenshot 2025-11-22 at 3 37 31 in the afternoon
iOS: Native Screenshot 2025-11-22 at 3 53 33 in the afternoon Screenshot 2025-11-22 at 3 53 24 in the afternoon Screenshot 2025-11-22 at 3 53 10 in the afternoon Screenshot 2025-11-22 at 3 53 05 in the afternoon Screenshot 2025-11-22 at 3 53 01 in the afternoon Screenshot 2025-11-22 at 3 52 57 in the afternoon
iOS: mWeb Safari Screenshot 2025-11-22 at 3 56 54 in the afternoon Screenshot 2025-11-22 at 3 56 36 in the afternoon Screenshot 2025-11-22 at 3 56 30 in the afternoon Screenshot 2025-11-22 at 3 56 25 in the afternoon Screenshot 2025-11-22 at 3 56 15 in the afternoon
MacOS: Chrome / Safari Screenshot 2025-11-22 at 3 59 05 in the afternoon Screenshot 2025-11-22 at 3 58 53 in the afternoon Screenshot 2025-11-22 at 3 58 48 in the afternoon Screenshot 2025-11-22 at 3 58 44 in the afternoon Screenshot 2025-11-22 at 3 58 33 in the afternoon
MacOS: Desktop Screenshot 2025-11-22 at 4 01 25 in the afternoon Screenshot 2025-11-22 at 4 01 15 in the afternoon Screenshot 2025-11-22 at 4 01 11 in the afternoon Screenshot 2025-11-22 at 4 01 04 in the afternoon Screenshot 2025-11-22 at 4 00 52 in the afternoon

@TaduJR TaduJR requested review from a team as code owners October 20, 2025 06:39
@melvin-bot melvin-bot Bot requested review from shubham1206agra and removed request for a team October 20, 2025 06:39
@melvin-bot

melvin-bot Bot commented Oct 20, 2025

Copy link
Copy Markdown

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

@TaduJR TaduJR closed this Oct 20, 2025
@TaduJR TaduJR force-pushed the feat-CFI-Add-group-by-report-layout-feature branch from f61de24 to adab45b Compare October 20, 2025 06:53
@TaduJR TaduJR reopened this Oct 20, 2025
@codecov

codecov Bot commented Oct 20, 2025

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 Δ
src/CONST/index.ts 85.84% <ø> (ø)
src/ONYXKEYS.ts 100.00% <ø> (ø)
src/SCREENS.ts 100.00% <ø> (ø)
src/libs/Navigation/linkingConfig/config.ts 75.00% <ø> (ø)
src/libs/ReportSecondaryActionUtils.ts 91.00% <100.00%> (+0.17%) ⬆️
src/styles/index.ts 45.65% <ø> (ø)
src/ROUTES.ts 12.15% <0.00%> (-0.13%) ⬇️
...gation/AppNavigator/ModalStackNavigators/index.tsx 8.42% <0.00%> (-0.02%) ⬇️
src/components/MoneyReportHeader.tsx 0.17% <0.00%> (-0.01%) ⬇️
src/libs/actions/ReportLayout.ts 0.00% <0.00%> (ø)
... and 4 more
... and 11 files with indirect coverage changes

@TaduJR

TaduJR commented Oct 20, 2025

Copy link
Copy Markdown
Contributor Author

Working on the unit tests. I will add them today/tomorrow. Until then @shubham1206agra can review how the is code looking with the feature request.

@trjExpensify

Copy link
Copy Markdown
Contributor

@TaduJR did you forget the video recording as well?

@TaduJR

TaduJR commented Oct 21, 2025

Copy link
Copy Markdown
Contributor Author

@TaduJR did you forget the video recording as well?

@trjExpensify 👋

Oh no I didn't. I am working on the tests steps since I will recording on based on the test steps I wanted a confirmation from you to see if it is sufficient after I finished writing it

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

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

@trjExpensify

Copy link
Copy Markdown
Contributor

Okay, can you please write the test steps and record he vids for the OP today? We're keen to get this shipped and out the door as customers are asking for it.

@Expensify/design I'm kicking off a build in case you want to review in the meantime to speed up any design feedback.

@github-actions

Copy link
Copy Markdown
Contributor

🧪🧪 Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing! 🧪🧪
Built from App PR #72943.

Android 🤖 iOS 🍎
https://ad-hoc-expensify-cash.s3.amazonaws.com/android/72943/index.html ❌ FAILED ❌
Android The QR code can't be generated, because the iOS build failed
Desktop 💻 Web 🕸️
❌ FAILED ❌ https://72943.pr-testing.expensify.com
The QR code can't be generated, because the Desktop build failed Web

👀 View the workflow run that generated this build 👀

@JmillsExpensify

JmillsExpensify commented Oct 21, 2025

Copy link
Copy Markdown
Contributor

Btw, I just tested the adhoc build. I'm curious for design feedback, though I'm I found the category totals a little odd to experience in practice.

For instance, it looks totally fine for this case, where they aren't too many expenses within a given category grouping.
CleanShot 2025-10-21 at 14 58 46@2x

On the other hand, when you have a lot of expenses within a given category, it's really easy to lose the context of the total.
CleanShot 2025-10-21 at 15 00 27

Now in the interest of urgency, this is fine for now, but I think it's something we should probably explore in future PRs. Do we show a summary of the category totals somewhere so it's easy to see them all and compare them? Do we "freeze" a given category total at the top of the expense table when it goes out of view? Those are things worth considering down the road to help make the groupings a bit more useful.

Again though, I think this is fine for now. I'd rather get this out and then tweak from there.

@trjExpensify

trjExpensify commented Oct 21, 2025

Copy link
Copy Markdown
Contributor

Took it for a quick spin myself as well. One thing functionally, it doesn't seem like the layout selected is persisting after simulating clearing the cache.

2025-10-21_14-07-05.mp4

I don't see the NVP in Applications > OnyxDB:

image

Interestingly, when I then switched over to Classic, I can see the tag value was remembered and stored in the layout NVP.

2025-10-21_14-22-18.mp4

@shubham1206agra

Copy link
Copy Markdown
Contributor

Took it for a quick spin myself as well. One thing functionally, it doesn't seem like the layout selected is persisting after simulating clearing the cache.
2025-10-21_14-07-05.mp4

I don't see the NVP in Applications > OnyxDB:
image

Interestingly, when I then switched over to Classic, I can see the tag value was remembered and stored in the layout NVP.
2025-10-21_14-22-18.mp4

@trjExpensify This is problem with BE. BE needs to send the Onyx key in OpenApp call.

@trjExpensify

Copy link
Copy Markdown
Contributor

Ah gotcha, so:

  • We're writing to and storing the mcc || tag value correctly in the expensify_groupByOption accountNVP when the user changes the setting. That explains why when I switch over to Classic, it's remembered.
  • We aren't include expensify_groupByOption in OpenApp yet, so if you sign-out and back in again, we don't add that NVP to your onyx data, and therefore revert back to the category layout which is the default without it set.

I'm cool to not block this PR on that, but we'll want to get that rectified pronto. Discussing that internally here with @nkuoch and @mountiny.

@dannymcclain

Copy link
Copy Markdown
Contributor

The build is looking as expected to me - though I will admit I don't love it. I think it certainly accomplishes the goal for now, but I'm not sure how I feel about this being the default report layout forever haha. Not a blocker, just something I'm going to keep in mind to explore down the line.

I also agree with Jason's point about finding a solution for totals, but ultimately feel the same as him here:

I think this is fine for now. I'd rather get this out and then tweak from there.

@TaduJR

TaduJR commented Oct 21, 2025

Copy link
Copy Markdown
Contributor Author

@trjExpensify

Wrote the test steps. Let me know what you think, and see if I missed case accidentally.

@trjExpensify

Copy link
Copy Markdown
Contributor

Great tests overall!

Verify groups are sorted by total amount (highest first)

On Classic we order on the category/tag values alphabetically (A>Z). We should do the same here.

image

@JmillsExpensify

Copy link
Copy Markdown
Contributor

@dannymcclain yeah maybe let's prioritize some explorations back in the lab? In the meantime, let's merge what we have and give our users what they want.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

The build is looking as expected to me - though I will admit I don't love it. I think it certainly accomplishes the goal for now, but I'm not sure how I feel about this being the default report layout forever haha. Not a blocker, just something I'm going to keep in mind to explore down the line.

100%. This feels overkill to me when you're looking at a report with 2-4 expenses especially given the repetitive categories:
CleanShot 2025-10-22 at 08 23 27@2x

From a visual perspective it looks good on desktop, but I kinda feel like mobile looks a bit unpolished. Wonder if we should drop the font size a bit on mobile given how quickly it truncates!?

CleanShot 2025-10-22 at 08 24 56@2x

@trjExpensify

Copy link
Copy Markdown
Contributor

From a visual perspective it looks good on desktop, but I kinda feel like mobile looks a bit unpolished. Wonder if we should drop the font size a bit on mobile given how quickly it truncates!?

Yeah, really good point. It feels pretty redundant on mobile when you can't see the totals. I feel like we need to show it all or not bother. FWIW, OldApp doesn't have this report layout stuff.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Also, multi-select on mobile gives me this weird red checkbox and the checkboxes aren't aligned:
CleanShot 2025-10-22 at 10 34 23@2x

We could not truncate on mobile and tighten the spacing a bit:

CleanShot 2025-10-22 at 10 36 13@2x

Still think it makes a normal report look way too overwhelming, but if we believe this is needed, then I think the above is a good tweak

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Sorry I guess the red checkbox should actually look like the one at the top given it's partially selected:
CleanShot 2025-10-22 at 10 37 47@2x

@TaduJR

TaduJR commented Oct 22, 2025

Copy link
Copy Markdown
Contributor Author

Great tests overall!

Thank you for your feedback!

On Classic we order on the category/tag values alphabetically (A>Z). We should do the same here.

On it.

Also, multi-select on mobile gives me this weird red checkbox and the checkboxes aren't aligned:

Will do that ASAP.

We could not truncate on mobile and tighten the spacing a bit:

Awesome, I like this a lot.

Sorry I guess the red checkbox should actually look like the one at the top given it's partially selected

Will do that as well.

What have decided on this comment?

@JS00001

JS00001 commented Nov 24, 2025

Copy link
Copy Markdown
Contributor

@nkuoch Do you want to review this or do you want me to take this issue to the finish line?

@TaduJR

TaduJR commented Nov 25, 2025

Copy link
Copy Markdown
Contributor Author

I think this is good to merge, thanks for the hard work!

My pleasure 🙏.

Thank you for your review.

@nkuoch nkuoch merged commit 13420bc into Expensify:main Nov 25, 2025
32 checks passed
Comment thread src/libs/ReportLayoutUtils.ts
@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/nkuoch in version: 9.2.64-0 🚀

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

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.2.64-5 🚀

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

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/nkuoch in version: 9.2.65-0 🚀

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

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.2.65-6 🚀

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.