Skip to content

Spend - Only "Current View" option is visible when selecting group row before expanding it. #99089

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.57-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #98716
Email or phone of affected tester (no customers): ibellicotest+2766@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Motorola MotoG60 / Android 12 (Hybrid app) - Windows 11 / Chrome
App Component: Search

Action Performed:

Prerequisite: Account has at least one workspace.

  1. Open the staging.new.expensify.com website.
  2. Navigate to workspace chat.
  3. Create at least two manual expenses with the same category.
  4. Navigate to "Spend" > "Expenses"
  5. Click on "Display" > "Group By" > "Category"
  6. Without expanding the displayed group, tick the group header checkbox.
  7. Click on dropdown menu.
  8. Note that the only option available, is "Current View"
  9. Expand the expenses group.
  10. Tick the group header checkbox again.
  11. Click on dropdown menu.
  12. Note that now, "Edit Multiple", "Hold"; "Move to report", "Duplicate Expenses" and "Delete" options appear.

Expected Result:

"Edit Multiple", "Hold", "Move to Report", "Duplicate Expenses", and "Delete" options should be always visible on dropdown menu after selecting a group row.

Actual Result:

Only "Current View" option is visible on dropdown menu after selecting expenses group before expanding it. "Edit Multiple", "Hold", "Move to Report", "Duplicate Expenses", and "Delete" options are not visible until expenses group is expanded for the first time.

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7238354_1787239473785.Grouped_Options.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7238354

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @jasperhuangg

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Aug 20, 2026
  2. applause-bot commented on Aug 20, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/a18b2844330b3c56b5a636901072a4db9cc946cc528234b8d6eb8612f77713df

  3. asmaatefomran commented on Aug 20, 2026

    @asmaatefomran

    Hi! I'd like to take this issue. I’ll investigate the group selection and bulk action menu behavior and open a PR with the fix. Could you please assign it to me?

  4. melvin-bot commented on Aug 20, 2026

    @melvin-bot

    📣 @asmaatefomran! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  5. MelvinBot commented on Aug 20, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Category groups load their child transactions lazily — a group's transactions array stays empty until the group is expanded once (expansion fires a search() for the group's transactionsQueryJSON sub-snapshot). So when you tick a group-header checkbox on a group that has never been expanded, the selection code receives an empty transactions array.

    In that empty case, toggle takes its "empty report" branch and stores a single placeholder entry keyed by the group key. That placeholder has every capability flag set to false (canHold, canChangeReport, …) and carries no underlying transaction. useSearchBulkActions gates Edit Multiple / Hold / Move to Report / Duplicate / Delete on those per-transaction flags, so they are all skipped — only the Export option survives, which collapses to its single "Current View" item.

    Once you expand the group, the sub-snapshot populates group.transactions, so re-selecting registers real per-transaction entries and the full option set appears — exactly the behavior described in the report.

    Investigation details (file:line evidence, confidence: High)

    Why Export still works while the group is collapsed: Export builds a whole-group query filter from the group key (useSearchBulkActions.ts:183), so it doesn't need loaded children — unlike the other actions.

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

    When a group-header checkbox is selected for a lazy group whose children haven't loaded yet, trigger the same fetch that expansion performs, so the existing reconcile logic converts the placeholder into real per-transaction entries (which then enable the full option set — no changes needed in useSearchBulkActions).

    Concretely, in toggle's empty-transactions branch at SearchWriteActionsProvider.tsx:467: distinguish a genuinely empty report from a not-yet-fetched lazy group using item.transactionsQueryJSON (a lazy group has one; an empty report does not — the same predicate GroupHeader uses for isEmpty). If it's a lazy group with no loaded children, still store the placeholder (so the checkbox shows selected immediately) and dispatch the same search({queryJSON: item.transactionsQueryJSON, …}) that GroupChildrenContent.refreshTransactions runs on expand. Once the sub-snapshot arrives, useSearchSnapshot enriches group.transactions and useReconcileSelectionWithData rewrites the selection into real per-transaction entries, enabling Edit / Hold / Move / Delete / Duplicate automatically.

    What alternative solutions did you explore? (Optional)

    Make useSearchBulkActions group-aware for Hold / Move / Delete / Duplicate so options can be computed synchronously for a still-collapsed group from group-level aggregates. This is a substantially larger change and duplicates capability logic, so the fetch-on-select approach is preferred. Note the recommended fix is asynchronous — options appear once the sub-snapshot loads (near-instant online).

    Possibly related (not a duplicate): #95369 — "Spend-Hybrid-Tap on checkbox does not select the group" (closed) — same group-checkbox area.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/99089) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  6. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Aug 20, 2026
  7. melvin-bot commented on Aug 20, 2026

    @melvin-bot

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

  8. melvin-bot commented on Aug 20, 2026

    @melvin-bot
  9. asmaatefomran commented on Aug 20, 2026

    @asmaatefomran

    Contributor details
    Your Expensify account email: asmaatef6789@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0121f2fae71a421cf3

  10. melvin-bot commented on Aug 20, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. Usama123talib commented on Aug 21, 2026

    @Usama123talib

    @truph01

    Proposal

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

    In Spend / Search with grouped rows (e.g. grouped by Category), selecting the checkbox on a collapsed group header only displays the "Current View" (under Export) option in the bulk actions menu. The expected bulk actions (Hold, Move to report, Delete, Duplicate, Edit multiple) do not appear until the user expands the group and selects it again.


    What is the root cause of that problem?

    1. Lazy Loading of Group Transactions:
      Grouped search results load child transactions lazily. A group's transactions array remains empty until the group is expanded, which triggers search({queryJSON: groupItem.transactionsQueryJSON, ...}) in GroupChildrenContent.tsx:92-99.

    2. Placeholder Selection with Disabled Capabilities:
      When the user checks the group header checkbox while the group is collapsed, GroupHeader.tsx:195-197 passes an empty groupItem.transactions array ([]) to toggle().
      In SearchWriteActionsProvider.tsx:471-486, toggle() enters the currentTransactions.length === 0 branch and creates a placeholder entry via mapEmptyReportToSelectedEntry(item).
      This placeholder entry has all action capabilities set to false (canHold: false, canChangeReport: false, canDelete: false, canDuplicate: false, etc.) because no underlying transactions are loaded yet.

    3. Bulk Actions Menu Filtering:
      useSearchBulkActions.ts gates each bulk action option on these capability flags. Because all flags on the placeholder are false, all actions (Hold, Move, Delete, Duplicate, etc.) are filtered out. Only the Export action is included, which collapses to the single "Current View" menu item.

    4. Missing Fetch on Selection:
      The group's transactions sub-snapshot is only fetched upon expanding the group (isExpanded), and is never triggered when selecting the group while collapsed.


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

    1. In SearchWriteActionsProvider.tsx, inside the toggle function's empty transactions branch (currentTransactions.length === 0), check if the selected item is a lazy group with a transactionsQueryJSON:
      • If item.transactionsQueryJSON exists and the group is being selected, dispatch search({ queryJSON: item.transactionsQueryJSON, offset: 0, shouldCalculateTotals: false, isLoading: false, isOffline }).
      • Store the initial group placeholder entry so the checkbox reflects the selected state immediately in the UI.
    if (currentTransactions.length === 0 && item.keyForList) {
        const reportKey = item.keyForList;
    
        if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
            return selectedTransactions;
        }
    
        if (selectedTransactions[reportKey]?.isSelected) {
            const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions};
            delete reducedSelectedTransactions[reportKey];
            return reducedSelectedTransactions;
        }
    
        // Trigger fetching the group's child transactions if it's an unexpanded lazy group
        if (item.transactionsQueryJSON) {
            search({
                queryJSON: item.transactionsQueryJSON,
                searchKey: undefined,
                offset: 0,
                shouldCalculateTotals: false,
                isLoading: false,
                isOffline,
            });
        }
    
        const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(item);
        return {...selectedTransactions, [reportKey]: emptyReportSelection};
    }
    1. Once the sub-snapshot arrives from the server/cache, the existing useReconcileSelectionWithData hook in SearchWriteActionsProvider.tsx:170-228 automatically maps the group selection into real per-transaction entries (mapTransactionItemToSelectedEntry) with active capability flags, enabling the full set of bulk actions (Hold, Move to report, Delete, Duplicate, Edit multiple) without requiring manual group expansion.

    What specific scenarios should we cover in automated tests to prevent regression?

    Add unit tests in tests/unit/SearchWriteActionsProviderTest.tsx:

    1. Selecting a collapsed group with transactionsQueryJSON:
      • Verify that search() is called with item.transactionsQueryJSON.
      • Verify that the group key is added to selectedTransactions.
    2. Reconciling after sub-snapshot arrives:
      • Verify that once group transactions are loaded in Onyx, useReconcileSelectionWithData replaces the group placeholder with populated per-transaction entries containing active action capabilities (canHold, canChangeReport, canDelete, etc.).
    3. Deselecting a collapsed group:
      • Verify that toggling the checkbox again correctly removes the group from selectedTransactions without redundant searches.

    What alternative solutions did you explore? (Optional)

    • Synchronously computing capabilities from group-level metadata: Calculating bulk actions based on group summaries without fetching child transactions. However, individual transaction permissions (e.g., whether specific expenses are held, policy permissions, report statuses) cannot be reliably determined without child transactions, and would duplicate permission logic across useSearchBulkActions. Fetching on selection leverages the existing reconciliation pipeline cleanly.

    Contributor Details

  12. truph01 commented on Aug 24, 2026

    @truph01
    Contributor

    @MelvinBot Your main solution could be problematic when there are many grouped transactions. For example, if there are 100 groups, clicking the header toggle would trigger 100 API calls.

    I don't think this is the best approach from a performance perspective.

  13. 18 remaining items

  14. truph01 commented on Sep 8, 2026

    @truph01
    Contributor

    Actual Result:
    Only "Current View" option is visible on dropdown menu after selecting expenses group before expanding it. "Edit Multiple", "Hold", "Move to Report", "Duplicate Expenses", and "Delete" options are not visible until expenses group is expanded for the first time.

    This is because a collapsed group carries no transaction data — the server sends only category / count / total / currency, and the children are fetched on expand. Hold / Move / Delete / Duplicate / Edit Multiple are gated on per-transaction flags, so they can't be enabled before that data exists.

    I want to hear the thoughts from internal/ @Expensify/design about it.

    🎀 👀 🎀 C+ reviewed

  15. melvin-bot commented on Sep 8, 2026

    @melvin-bot

    Triggered auto assignment to @jasperhuangg, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

  16. shawnborton commented on Sep 8, 2026

    @shawnborton
    Contributor

    Hmm @JmillsExpensify @JS00001 @trjExpensify what would be the expected behavior here for selecting groups in Spend?

  17. JS00001 commented on Sep 8, 2026

    @JS00001
    Contributor

    Only "Current View" option is visible on dropdown menu after selecting expenses group before expanding it. "Edit Multiple", "Hold", "Move to Report", "Duplicate Expenses", and "Delete" options are not visible until expenses group is expanded for the first time.

    I dont remember ever implementing these for groups. This feels expected, but maybe we want to implement it as a new feature?

  18. trjExpensify commented on Sep 8, 2026

    @trjExpensify
    Contributor

    Right, we don't know enough about the expenses nested within the group to give you expense level action options until you expand one of them, right? IIRC, that was a big no-no for performance in the initial implementation?

    What you do get without expanding the group is the option to export the groups and the transaction data, but we do that group export server-side.

  19. JS00001 commented on Sep 9, 2026

    @JS00001
    Contributor

    Yeah, thats correct

  20. melvin-bot commented on Sep 11, 2026

    @melvin-bot

    @jasperhuangg Whoops! This issue is 2 days overdue. Let's get this updated quick!

  21. melvin-bot commented on Sep 15, 2026

    @melvin-bot

    @jasperhuangg 6 days overdue. This is scarier than being forced to listen to Vogon poetry!

  22. truph01 commented on Sep 16, 2026

    @truph01
    Contributor

    @JS00001 So what should we do next in this issue?

  23. JS00001 commented on Sep 16, 2026

    @JS00001
    Contributor

    I think we should probably close this as expected for the time being. I dont think we want to start returning all of the results up front, we should stick with the current implementation

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions