Skip to content

[Due for payment 2026-01-25] [$125] The report carousel is disappearing when report is Approved #79082

Closed

Description

@mountiny

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: v9.2.95-3
Reproducible in staging?: Y
Reproducible in production?: Y
**If this was caught during regression testing, add the test name, ID and link from BrowserStack:**
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
**Expensify/Expensify Issue URL:

**Issue reported by: @JmillsExpensify
Slack conversation (hyperlinked to channel name): https://expensify.slack.com/archives/C05LX9D6E07/p1767634292638219

Action Performed:

Break down in numbered steps

Prerequisite: Be an approver on a policy with multiple reports awaiting your approval

  1. Go to Reports > Approve
  2. Verify that you can see the 1 of X Arrows carousel to cycle through the other reports to approve
  3. Approve the report

Expected Result:

Describe what you think should've happened

Verify that you can see the 1 of X-1 Arrows carousel still and you can go to next report to approve

Actual Result:

Describe what actually happened

The carousel disappeared

Workaround:

Can the user still use Expensify without this being fixed? Have you informed them of the workaround?

N/A

Platforms:

Select the officially supported platforms where the issue was reproduced:

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

Screenshots/Videos

View all open jobs on GitHub

Issue Owner

Current Issue Owner: @

Issue Owner

Current Issue Owner: @kadiealexander

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Jan 8, 2026
  2. melvin-bot commented on Jan 8, 2026

    @melvin-bot

    Triggered auto assignment to @kadiealexander (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

  3. twinkle-react commented on Jan 8, 2026

    @twinkle-react

    Proposal

    Please re-state the problem that we are trying to solve in this issue.
    The report carousel disappears when a report is approved from the search view. In APPROVE, UNAPPROVED_CASH, and UNAPPROVED_CARD searches, users see a carousel (e.g., “1 of X”) to navigate between reports. After approving one report, the carousel disappears instead of updating to reflect the remaining reports (e.g., “1 of X-1”) and allowing continued navigation.

    What is the root cause of that problem?
    In approveMoneyRequestOnSearch, when a report is approved from certain suggested searches (APPROVE, UNAPPROVED_CASH, UNAPPROVED_CARD), the implementation immediately removes the approved report from the search snapshot by setting its entry to null in ONYXKEYS.COLLECTION.SNAPSHOT. This prematurely mutates the snapshot data, causing the allReports array used by MoneyRequestReportNavigation to shrink unexpectedly and break the carousel navigation logic.

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

    Remove the logic that immediately removes approved reports from the search snapshot data. Instead, keep the snapshot intact so the carousel can continue to function correctly. The search results can refresh naturally later or reflect updated report status without deleting entries mid-navigation.

    Code change (remove snapshot mutation):

    function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], currentSearchKey?: SearchKey) {
        const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA>> = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(
                    reportIDList.map((reportID) => [
                        `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
                        {isActionLoading: true},
                    ]),
                ),
            },
        ];
    
        const successData: Array<
            OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.SNAPSHOT>
        > = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(
                    reportIDList.map((reportID) => [
                        `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
                        {isActionLoading: false},
                    ]),
                ),
            },
        ];
    
        //  Remove this entire block that removes reports from snapshot data
        // const approveActionSuggestedSearches: Partial<SearchKey[]> = [
        //     CONST.SEARCH.SEARCH_KEYS.APPROVE,
        //     CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH,
        //     CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD,
        // ];
        // if (approveActionSuggestedSearches.includes(currentSearchKey)) {
        //     successData.push({
        //         onyxMethod: Onyx.METHOD.MERGE,
        //         key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
        //         value: {
        //             data: Object.fromEntries(
        //                 reportIDList.map((reportID) => [
        //                     `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
        //                     null,
        //                 ]),
        //             ),
        //         },
        //     });
        // }
    
        const failureData: Array<
            OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT>
        > = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(
                    reportIDList.map((reportID) => [
                        `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
                        {isActionLoading: false},
                    ]),
                ),
            },
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT,
                value: Object.fromEntries(
                    reportIDList.map((reportID) => [
                        `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
                        {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
                    ]),
                ),
            },
        ];
    
        playSound(SOUNDS.SUCCESS);
        API.write(
            WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH,
            {hash, reportIDList},
            {optimisticData, failureData, successData},
        );
    }

    What alternative solutions did you explore? (Optional)

    N/A

  4. yatin-italy commented on Jan 8, 2026

    @yatin-italy

    Proposal

    Please re-state the problem that we are trying to solve in this issue.
    The report carousel disappears after a report is approved from the search view. In approval-related searches (APPROVE, UNAPPROVED_CASH, UNAPPROVED_CARD), users can navigate between multiple reports using a carousel (for example, “1 of X”). After approving a report, the carousel is removed instead of updating to reflect the remaining reports and allowing continued navigation.

    What is the root cause of that problem?
    When approving a report from certain suggested searches, the approved report is removed from the search snapshot data. This causes the list of reports used by the carousel navigation to shrink unexpectedly, which breaks the navigation logic that depends on a stable set of reports.

    What changes do you think we should make in order to solve the problem?
    Instead of removing the approved report from the snapshot, update the report’s status within the snapshot data to reflect that it has been approved. This preserves the report list used by the carousel while still representing the updated approval state.

    function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], currentSearchKey?: SearchKey) {
        const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA>> = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: true}])),
            },
        ];
    
        const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])),
            },
        ];
    
        // If we are on the 'Approve', `Unapproved cash` or the `Unapproved company cards` suggested search, update the report status instead of removing it
        const approveActionSuggestedSearches: Partial<SearchKey[]> = [
            CONST.SEARCH.SEARCH_KEYS.APPROVE,
            CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH,
            CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD,
        ];
    
        if (approveActionSuggestedSearches.includes(currentSearchKey)) {
            successData.push({
                onyxMethod: Onyx.METHOD.MERGE,
                key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
                value: {
                    data: Object.fromEntries(
                        reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {stateNum: CONST.REPORT.STATE_NUM.APPROVED}]),
                    ),
                },
            });
        }
    
        const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT>> = [
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT_METADATA,
                value: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])),
            },
            {
                onyxMethod: Onyx.METHOD.MERGE_COLLECTION,
                key: ONYXKEYS.COLLECTION.REPORT,
                value: Object.fromEntries(
                    reportIDList.map((reportID) => [
                        `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
                        {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
                    ]),
                ),
            },
        ];
    
        playSound(SOUNDS.SUCCESS);
        API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData});
    }

    What alternative solutions did you explore? (Optional)

    • Removing the logic that deletes reports from the snapshot and relying on a later search refresh
  5. TaduJR commented on Jan 8, 2026

    @TaduJR
    Contributor

    Proposal

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

    When an approver on a policy with multiple reports awaiting approval goes to Reports > Approve and approves a report, the navigation carousel (showing "1 of X" arrows to cycle through reports) disappears instead of updating to "1 of X-1" and allowing navigation to the next report.

    What is the root cause of that problem?

    The root cause is that approveMoneyRequest does not explicitly remove the approved report from the search snapshot, unlike approveMoneyRequestOnSearch which does.

    Root Cause Flow:

    1. User approves report via confirmApproval in MoneyReportHeader.tsx which calls approveMoneyRequest:

    const confirmApproval = () => {
    if (hasDynamicExternalWorkflow(policy)) {
    showDWEModal();
    return;
    }
    setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
    if (isDelegateAccessRestricted) {
    showDelegateNoAccessModal();
    } else if (isAnyTransactionOnHold) {
    setIsHoldMenuVisible(true);
    } else {
    startApprovedAnimation();
    approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, true);
    }
    };
    const markAsCash = useCallback(() => {
    if (!requestParentReportAction) {
    return;
    }
    const reportID = transactionThreadReport?.reportID;
    if (!iouTransactionID || !reportID) {
    return;
    }
    markAsCashAction(iouTransactionID, reportID, transactionViolations);
    }, [iouTransactionID, requestParentReportAction, transactionThreadReport?.reportID, transactionViolations]);

    1. approveMoneyRequest optimistically updates the report's statusNum from 1 (SUBMITTED) to 3 (APPROVED) in the main report collection:

    },
    };
    const optimisticIOUReportData: OnyxUpdate = {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
    value: updatedExpenseReport,
    };
    let optimisticChatReportData: OnyxUpdate | undefined;
    if (chatReport) {
    optimisticChatReportData = {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.chatReportID}`,
    value: {
    hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, updatedExpenseReport, currentUserEmail),
    },
    };

    1. Onyx's merge behavior propagates this update to the snapshot's nested data.report_<id> because the key format ${ONYXKEYS.COLLECTION.REPORT}${reportID} matches the snapshot's nested data keys.

    2. The filtering logic in getReportSections uses actionFilterMapping['approve'] which maps to expenseStatusActionMapping['outstanding'], requiring statusNum === 1:

    const actionFilterMapping = {
    [CONST.SEARCH.ACTION_FILTERS.SUBMIT]: expenseStatusActionMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS],
    [CONST.SEARCH.ACTION_FILTERS.APPROVE]: expenseStatusActionMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING],

    1. Since the snapshot's report now has statusNum=3, it's filtered out (shouldShow: false):

    App/src/libs/SearchUIUtils.ts

    Lines 1740 to 1755 in a0bb986

    }
    const actionFromQuery = queryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION)?.filters?.at(0)?.value;
    if (actionFromQuery && isValidActionFilter(actionFromQuery)) {
    shouldShow = shouldShow && actionFilterMapping[actionFromQuery](reportItem);
    }
    }
    if (shouldShow) {
    const reportPendingAction = reportItem?.pendingAction ?? reportItem?.pendingFields?.preview;
    const shouldShowBlankTo = !reportItem || isOpenExpenseReport(reportItem);
    const allActions = getActions(data, allViolations, key, currentSearch, currentUserEmail, actions);
    const fromDetails =
    data.personalDetailsList?.[reportItem.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ??
    getPersonalDetailsForAccountID(reportItem.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ??

    1. With allReports.length dropping and currentIndex becoming -1, the visibility condition fails:

    const shouldDisplayNavigationArrows = isExpenseReportSearch && allReports && allReports.length > 1 && currentIndex !== -1 && !!lastSearchQuery?.queryJSON;

    Key Difference: approveMoneyRequestOnSearch explicitly removes the report from snapshot on success:

    // If we are on the 'Approve', `Unapproved cash` or the `Unapproved company cards` suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search
    const approveActionSuggestedSearches: Partial<SearchKey[]> = [CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD];
    if (approveActionSuggestedSearches.includes(currentSearchKey)) {
    successData.push({
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
    value: {
    data: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])),
    },
    });
    }

    But approveMoneyRequest does not do this.

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

    Solution: Explicitly remove the approved report from the snapshot in confirmApproval, matching the pattern used in approveMoneyRequestOnSearch.

    Change in MoneyReportHeader.tsx:

    const confirmApproval = () => {
    if (hasDynamicExternalWorkflow(policy)) {
    showDWEModal();
    return;
    }
    setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
    if (isDelegateAccessRestricted) {
    showDelegateNoAccessModal();
    } else if (isAnyTransactionOnHold) {
    setIsHoldMenuVisible(true);
    } else {
    startApprovedAnimation();
    approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, true);
    }
    };
    const markAsCash = useCallback(() => {
    if (!requestParentReportAction) {
    return;
    }
    const reportID = transactionThreadReport?.reportID;
    if (!iouTransactionID || !reportID) {
    return;
    }
    markAsCashAction(iouTransactionID, reportID, transactionViolations);
    }, [iouTransactionID, requestParentReportAction, transactionThreadReport?.reportID, transactionViolations]);

    const confirmApproval = () => {
        if (hasDynamicExternalWorkflow(policy)) {
            showDWEModal();
            return;
        }
        setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
        if (isDelegateAccessRestricted) {
            showDelegateNoAccessModal();
        } else if (isAnyTransactionOnHold) {
            setIsHoldMenuVisible(true);
        } else {
            startApprovedAnimation();
            approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, true);
    
            // Remove the approved report from the search snapshot to prevent filtering issues
            // This mirrors the pattern used in approveMoneyRequestOnSearch
            if (currentSearchQueryJSON?.hash && moneyRequestReport?.reportID) {
                const approveActionSuggestedSearches = [
                    CONST.SEARCH.SEARCH_KEYS.APPROVE,
                    CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH,
                    CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD,
                ];
                if (approveActionSuggestedSearches.includes(currentSearchKey)) {
                    Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, {
                        data: {
                            [`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport.reportID}`]: null,
                        },
                    });
                }
            }
        }
    };

    This fix:

    1. Explicitly removes the approved report from the snapshot by setting it to null
    2. Only applies when in an approve-related search context (matching the logic in approveMoneyRequestOnSearch)
    3. Prevents the filtering from excluding a "half-updated" report
    4. Allows the carousel to correctly update to show remaining reports
  6. TaduJR commented on Jan 8, 2026

    @TaduJR
    Contributor

    Demo of Proposal

    Mac-Chrome.mp4
  7. huult commented on Jan 8, 2026

    @huult
    Contributor

    Proposal

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

    The report carousel is disappearing when report is Approved

    What is the root cause of that problem?

    This issue was caused by this PR #77957. In that change, we started filtering search data when the action key is present, such as submit, approve, or pay to fix the issue #76519.

    App/src/libs/SearchUIUtils.ts

    Lines 1748 to 1752 in 727f5f9

    const actionFromQuery = queryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION)?.filters?.at(0)?.value;
    if (actionFromQuery && isValidActionFilter(actionFromQuery)) {
    shouldShow = shouldShow && actionFilterMapping[actionFromQuery](reportItem);
    }

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

    To resolve this issue, we don’t need to apply search-data filtering when an action key is present for MoneyRequestReportNavigation. We can implement it like this:

    App/src/libs/SearchUIUtils.ts

    Lines 1750 to 1752 in 727f5f9

    if (actionFromQuery && isValidActionFilter(actionFromQuery)) {
    shouldShow = shouldShow && actionFilterMapping[actionFromQuery](reportItem);
    }

    update to:

    if (!shouldSkipActionFiltering && actionFromQuery && isValidActionFilter(actionFromQuery)) {
        shouldShow = shouldShow && actionFilterMapping[actionFromQuery](reportItem);
    }

    add:

    shouldSkipActionFiltering: true,

    This is a POC for this ticket:

    Screen.Recording.2026-01-09.at.01.22.13.mov

    This is a POC for #76519 after we applied my solution:

    Screen.Recording.2026-01-09.at.01.22.57.mov

    Test branch

    What alternative solutions did you explore? (Optional)

    Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.

  8. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Jan 8, 2026
  9. 51 remaining items

  10. changed the title [-][$250] The report carousel is disappearing when report is Approved[/-] [+][$125] The report carousel is disappearing when report is Approved[/+] on Jan 21, 2026
  11. melvin-bot commented on Jan 21, 2026

    @melvin-bot

    Upwork job price has been updated to $125

  12. mountiny commented on Jan 21, 2026

    @mountiny
    ContributorAuthor
  13. huult commented on Jan 21, 2026

    @huult
    Contributor

    Ah, got it. I understand now. Thank you very much!

  14. mountiny commented on Jan 24, 2026

    @mountiny
    ContributorAuthor

    Awaiting payment

  15. changed the title [-][$125] The report carousel is disappearing when report is Approved[/-] [+][Due for payment 2026-01-25] [$125] The report carousel is disappearing when report is Approved[/+] on Jan 24, 2026
  16. kadiealexander commented on Jan 27, 2026

    @kadiealexander
    Contributor

    Payment Summary

  17. mkhutornyi commented on Jan 27, 2026

    @mkhutornyi
    Contributor

    I'll be getting paid via newDot

  18. quinthar commented on Feb 7, 2026

    @quinthar
    Contributor

    Approved $125 to @mkhutornyi

  19. melvin-bot commented on Apr 30, 2026

    @melvin-bot

    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

    If a regression has occurred and you are the assigned CM follow the instructions here.

    If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething 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