From 3249384825f6bb51f6eb19c0440c573c549acbb6 Mon Sep 17 00:00:00 2001 From: "Vit Horacek (via MelvinBot)" Date: Wed, 21 Oct 2026 22:18:05 +0000 Subject: [PATCH] Revert "Merge pull request #103703 from Expensify/claude-reapplySingleExpenseReportViewToggle" This reverts commit 7cb9aa2eaa61481ca71789877b95a36265e0d49d, reversing changes made to 7255f7a9f9cd406a140ca830b2b3858fa441b068. Co-authored-by: Vit Horacek --- src/CONST/index.ts | 6 - src/ONYXKEYS.ts | 4 - src/components/BulkActionBar/types.ts | 4 +- src/components/MoneyReportHeader.tsx | 8 +- .../MoneyReportHeaderSecondaryActions.tsx | 17 +- .../MoneyReportHeaderActions/index.tsx | 6 +- .../MoneyRequestReportActionsList.tsx | 5 +- .../MoneyRequestReportView.tsx | 7 +- .../SelectionBulkActionBar.tsx | 7 +- .../SelectionToolbar/index.tsx | 20 +- .../ReportSubmitToPopoverAnchor.tsx | 6 - src/components/Search/index.tsx | 11 +- src/hooks/useExpenseActions.ts | 11 +- src/hooks/useSingleExpenseReportView.ts | 41 ---- src/hooks/useTransactionThreadReportID.ts | 6 +- src/languages/de.ts | 2 - src/languages/el.ts | 2 - src/languages/en.ts | 2 - src/languages/es.ts | 2 - src/languages/fr.ts | 2 - src/languages/it.ts | 2 - src/languages/ja.ts | 2 - src/languages/nl.ts | 2 - src/languages/pl.ts | 2 - src/languages/pt-BR.ts | 2 - src/languages/zh-hans.ts | 2 - src/libs/ExportOnyxState/masking.ts | 1 - src/libs/MoneyRequestReportUtils.ts | 28 +-- .../AppNavigator/AuthScreensInitHandler.tsx | 6 +- .../shouldShowPushNotification.ts | 18 +- src/libs/PopoverMenuSections.ts | 2 +- src/libs/ReportSecondaryActionUtils.ts | 8 - src/libs/TransactionUtils/duplicates.ts | 12 -- src/libs/TransactionUtils/index.ts | 2 - src/libs/actions/ReportLayout.ts | 35 +--- .../hooks/useReportDetailsDeleteModal.ts | 20 +- .../Search/SearchMoneyRequestReportPage.tsx | 9 +- .../DynamicConfirmationPage.tsx | 22 +- .../OneTransactionThreadRedirectHandler.tsx | 8 +- src/pages/inbox/ReportActions.tsx | 4 +- src/pages/inbox/WideRHPReceiptPanel.tsx | 11 +- .../useComposerReportData.ts | 6 +- src/selectors/ReportAction.ts | 5 - tests/actions/ReportLayoutTest.ts | 41 +--- ...neTransactionThreadRedirectHandlerTest.tsx | 50 ----- tests/ui/AuthScreensInitHandlerTest.tsx | 29 --- ...eaderActionsSingleExpenseTableViewTest.tsx | 95 --------- .../MoneyReportHeaderSelectionModeTest.tsx | 37 ---- .../SelectionToolbarSubmitToPopoverTest.tsx | 99 --------- tests/unit/MoneyRequestReportUtilsTest.ts | 66 ------ ...rtActionsWithInboxTabDeferredMountTest.tsx | 2 +- tests/unit/ReportSecondaryActionUtilsTest.ts | 112 ----------- tests/unit/TransactionUtilsTest.ts | 41 ---- ...ctionsSingleExpenseTableViewDeleteTest.tsx | 190 ------------------ .../hooks/useSingleExpenseReportView.test.ts | 65 ------ .../useTransactionThreadReportID.test.ts | 37 +--- .../useReportDetailsDeleteModalTest.ts | 104 +--------- tests/unit/shouldShowPushNotificationTest.ts | 87 -------- 58 files changed, 50 insertions(+), 1383 deletions(-) delete mode 100644 src/hooks/useSingleExpenseReportView.ts delete mode 100644 tests/ui/components/MoneyReportHeaderActionsSingleExpenseTableViewTest.tsx delete mode 100644 tests/ui/components/SelectionToolbarSubmitToPopoverTest.tsx delete mode 100644 tests/unit/hooks/useExpenseActionsSingleExpenseTableViewDeleteTest.tsx delete mode 100644 tests/unit/hooks/useSingleExpenseReportView.test.ts delete mode 100644 tests/unit/shouldShowPushNotificationTest.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 7e850c8b2da0..89fa136a9c05 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1946,7 +1946,6 @@ const CONST = { DUPLICATE_EXPENSE: 'duplicateExpense', DUPLICATE_REPORT: 'duplicateReport', MOVE_EXPENSE: 'moveExpense', - TOGGLE_SINGLE_EXPENSE_VIEW: 'toggleSingleExpenseView', }, PRIMARY_ACTIONS: { SUBMIT: 'submit', @@ -2509,10 +2508,6 @@ const CONST = { DETAILED: 'detailed', MATRIX: 'matrix', }, - SINGLE_EXPENSE_REPORT_VIEW: { - EXPENSE: 'expense', - TABLE: 'table', - }, } as const, UNREPORTED_EXPENSES_PAGE_SIZE: 50, COMPOSER: { @@ -10180,7 +10175,6 @@ const CONST = { PAY: 'MoreMenu-Pay', DUPLICATE_REPORT: 'MoreMenu-DuplicateReport', MOVE_EXPENSE: 'MoreMenu-MoveExpense', - TOGGLE_SINGLE_EXPENSE_VIEW: 'MoreMenu-ToggleSingleExpenseView', }, REPORT_PREVIEW: { CARD: 'ReportPreview-Card', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9313563cc256..589c104bf1dc 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -939,9 +939,6 @@ const ONYXKEYS = { /** Dragged table column widths, keyed by `columnResizingID` then column key. Per device, since ideal widths depend on window size. */ TABLE_COLUMN_WIDTHS: 'tableColumnWidths', - /** Stores whether the user views single-expense reports in the expense view or the table view */ - NVP_SINGLE_EXPENSE_REPORT_VIEW: 'nvp_expensify_singleExpenseReportView', - /** Partial transaction data used for MFA authorize transaction preview */ TRANSACTIONS_PENDING_3DS_REVIEW: 'transactionsPending3DSReview', @@ -2038,7 +2035,6 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_REPORT_LAYOUT_OPTION]: string; [ONYXKEYS.NVP_REPORT_DETAILS_COLUMNS]: string[]; [ONYXKEYS.TABLE_COLUMN_WIDTHS]: OnyxTypes.TableColumnWidths; - [ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW]: ValueOf; [ONYXKEYS.HAS_DENIED_CONTACT_IMPORT_PROMPT]: boolean | undefined; [ONYXKEYS.PERSONAL_POLICY_ID]: string; [ONYXKEYS.RAM_ONLY_EXPENSE_ADDED_GROWL_TRANSACTION_IDS]: Record; diff --git a/src/components/BulkActionBar/types.ts b/src/components/BulkActionBar/types.ts index db3ad6f4f6f4..586ec4c3d5a7 100644 --- a/src/components/BulkActionBar/types.ts +++ b/src/components/BulkActionBar/types.ts @@ -1,7 +1,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import type {ComponentRef, Ref} from 'react'; +import type {ComponentRef, RefObject} from 'react'; import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; type BulkActionBarProps = { @@ -41,7 +41,7 @@ type BulkActionBarProps = { * Anchor for popovers a caller opens against the bar, such as the KYC wall Search puts behind its pay action. * Attached to the bar itself, since which button opened the flow is not something the bar exposes. */ - barRef?: Ref>; + barRef?: RefObject | null>; /** * Extra styles for the absolutely positioned layer the bar floats in. Pass a `bottom` here to lift the bar above diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 56ffa6a2112d..84c1bbdc682d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -7,7 +7,6 @@ import useOnyx from '@hooks/useOnyx'; import useReportPrimaryAction from '@hooks/useReportPrimaryAction'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; @@ -135,7 +134,6 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout; const isMobileSelectionModeEnabled = useMobileSelectionMode(); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(moneyRequestReport?.reportID); const isFocused = useIsFocused(); useEffect(() => { @@ -145,13 +143,11 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt }, []); if (isMobileSelectionModeEnabled && shouldUseNarrowLayout && isFocused) { - // If mobile selection mode is enabled but too few transactions remain to show the table, turn it off. A single - // expense still shows the table when the user chose the table view for single-expense reports. The selection mode + // If mobile selection mode is enabled but only one or no transactions remain, turn it off. The selection mode // is shared with every screen, so this report only gets to turn it off while it is the focused one. Another // screen on top of it, such as the add existing expense modal, owns the mode for as long as it is open. const visibleTransactions = transactions.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); - const minTransactionsForSelection = shouldUseTableViewForSingleExpense ? 1 : 2; - if (visibleTransactions.length < minTransactionsForSelection) { + if (visibleTransactions.length <= 1) { turnOffMobileSelectionMode(); } diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index 866ead5ca229..232c7ee9d195 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -36,14 +36,12 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useShareActions from '@hooks/useShareActions'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import useVerifyAccountAndResume from '@hooks/useVerifyAccountAndResume'; import {generateDefaultWorkspaceName} from '@libs/actions/Policy/Policy'; -import {setSingleExpenseReportView} from '@libs/actions/ReportLayout'; import {search} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getPlatform from '@libs/getPlatform'; @@ -120,7 +118,6 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo }); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const {singleExpenseReportView, shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(moneyRequestReport?.reportID); const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasSeenTourSelector, }); @@ -321,7 +318,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo ? sortPoliciesByName(activeAdminPolicies, localeCompare) : []; - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Info', 'Cash', 'ArrowRight', 'Building', 'Table', 'Receipt']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Info', 'Cash', 'ArrowRight', 'Building']); // Build PAY action sub-items. Workspace-policy entries carry the policy as data and have no onSelected; // MoneyReportHeaderKYCDropdown picks them up via onSubItemSelected where triggerKYCFlow is in scope. @@ -426,18 +423,6 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo // Merge all action implementations const secondaryActionsImplementation: Record = { - [CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW]: { - value: CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW, - text: shouldUseTableViewForSingleExpense ? translate('reportLayout.switchToExpenseView') : translate('reportLayout.switchToTableView'), - icon: shouldUseTableViewForSingleExpense ? expensifyIcons.Receipt : expensifyIcons.Table, - sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.TOGGLE_SINGLE_EXPENSE_VIEW, - onSelected: () => { - setSingleExpenseReportView( - shouldUseTableViewForSingleExpense ? CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE : CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE, - singleExpenseReportView, - ); - }, - }, [CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: { value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS, text: translate('iou.viewDetails'), diff --git a/src/components/MoneyReportHeaderActions/index.tsx b/src/components/MoneyReportHeaderActions/index.tsx index a2ceb7f1c384..a564fad4a815 100644 --- a/src/components/MoneyReportHeaderActions/index.tsx +++ b/src/components/MoneyReportHeaderActions/index.tsx @@ -8,7 +8,6 @@ import useLayoutSpacing from '@hooks/useLayoutSpacing'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -57,10 +56,7 @@ function MoneyReportHeaderActions({reportID, primaryAction, isReportInSearch, ba const {selectedTransactionIDs} = useSearchSelectionContext(); const {clearSelectedTransactions} = useSearchSelectionActions(); const hasSelectedTransactions = !!selectedTransactionIDs.length; - - // In the table view a single-expense report lists its expense as a selectable row, so it acts like a multi-expense report here. - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(moneyRequestReport?.reportID); - const isTransactionThread = !!transactionThreadReportID && !shouldUseTableViewForSingleExpense; + const isTransactionThread = !!transactionThreadReportID; useEffect(() => { if (!transactionThreadReportID) { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 6368c9b974dd..490513e7e1e4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -10,7 +10,6 @@ import {usePersonalDetailsByIDs} from '@hooks/usePersonalDetails'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportsParentHierarchy from '@hooks/useReportsParentHierarchy'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useThemeStyles from '@hooks/useThemeStyles'; import useUnreadMarker from '@hooks/useUnreadMarker'; @@ -132,7 +131,6 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions, false, reportTransactionIDs); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(reportID); const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived); @@ -195,8 +193,7 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon const {markNewestActionAsRead, completeSkippedMarkAsRead} = useMarkAsRead({ reportID: reportID ?? reportIDFromRoute ?? '', report, - // The table view doesn't show the expense thread's actions, so a new one there shouldn't mark the report as read - transactionThreadReport: shouldUseTableViewForSingleExpense ? undefined : transactionThreadReport, + transactionThreadReport, sortedVisibleReportActions: visibleReportActionsNewestFirst, sortedReportActions: reportActions, isScrolledToEnd: !hasScrolledOverThreshold, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 66e4841726e0..ebef8dc41714 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -14,7 +14,6 @@ import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useThemeStyles from '@hooks/useThemeStyles'; import {removeFailedReport} from '@libs/actions/Report'; @@ -142,7 +141,6 @@ function MoneyRequestReportView({report, reportIDFromRoute, reportLoadingState, const reportErrors = visibleTransactions.length === 1 && visibleTransactions.at(0)?.errors ? undefined : allReportErrors; const reportTransactionIDs = visibleTransactions.map((transaction) => transaction.transactionID); const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(reportID); const isReportLoadPending = useIsReportLoadPending(reportID); const dismissReportCreationError = () => { @@ -160,11 +158,10 @@ function MoneyRequestReportView({report, reportIDFromRoute, reportLoadingState, const shouldShowOpenReportLoadingSkeleton = !!(isReportLoadPending && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions; const isEmptyTransactionReport = visibleTransactions?.length === 0 && transactionThreadReportID === undefined; - const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? [], shouldUseTableViewForSingleExpense); + const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); - // The side-by-side receipt belongs to the single-expense view, so it's hidden when the user picks the table view - const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport && !shouldUseTableViewForSingleExpense; + const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport; const reportHeaderView = isTransactionThreadView ? ( { if (isSecondaryActionAWorkspacePolicyOption(item)) { diff --git a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx index 218107f18f67..fa044fb39432 100644 --- a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx +++ b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx @@ -3,7 +3,7 @@ import {ModalActions} from '@components/Modal/Global/ModalContext'; import useShouldShowReportBulkActionBar from '@components/MoneyRequestReportView/useShouldShowReportBulkActionBar'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu'; -import {ReportSubmitToPopoverMeasurableAnchor, ReportSubmitToPopoverRoot} from '@components/ReportSubmitToPopoverAnchor'; +import {ReportSubmitToPopoverAnchor} from '@components/ReportSubmitToPopoverAnchor'; import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler'; import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; @@ -277,7 +277,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool - + - + - - + ); } diff --git a/src/components/ReportSubmitToPopoverAnchor.tsx b/src/components/ReportSubmitToPopoverAnchor.tsx index 33246850711d..9c4785f74222 100644 --- a/src/components/ReportSubmitToPopoverAnchor.tsx +++ b/src/components/ReportSubmitToPopoverAnchor.tsx @@ -59,11 +59,6 @@ function useOpenReportSubmitToPopover(): OpenReportSubmitToPopover { return useContext(ReportSubmitToPopoverContext); } -/** The ref the submit-to popover measures, for anchors that can't be wrapped in {@link ReportSubmitToPopoverMeasurableAnchor}. */ -function useReportSubmitToPopoverAnchorRef() { - return useContext(ReportSubmitToPopoverAnchorRefContext); -} - /** Opens the shared Search submit-to popover for a report (e.g. bulk actions outside a list row). */ function useOpenSearchReportSubmitToPopover() { const host = useContext(ReportSubmitToPopoverHostContext); @@ -327,6 +322,5 @@ export { SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT, useOpenReportSubmitToPopover, useOpenSearchReportSubmitToPopover, - useReportSubmitToPopoverAnchorRef, useSearchSubmitPopoverGuard, }; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 4c1c1c4cf42d..262d9cdacfca 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -19,7 +19,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSaveSortedReportIDs from '@hooks/useSaveSortedReportIDs'; import useSearchAutoRefetch from '@hooks/useSearchAutoRefetch'; import useSearchShouldCalculateTotals, {getSearchRequestOffsetForMissingAllMatchingCount} from '@hooks/useSearchShouldCalculateTotals'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useStableArrayReference from '@hooks/useStableArrayReference'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -35,14 +34,13 @@ import { shouldRefreshActiveTransactionIDs, } from '@libs/actions/TransactionThreadNavigation'; import Log from '@libs/Log'; -import {shouldUseMultiExpenseReportLayout} from '@libs/MoneyRequestReportUtils'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import openInternalRouteInNewTab, {isModifiedMousePress} from '@libs/Navigation/helpers/openInternalRouteInNewTab'; import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {flushPendingSearchWrite, hasPendingSearchWrite} from '@libs/pendingSearchWrite'; -import {isCreatedTaskReportAction, isSentMoneyReportAction} from '@libs/ReportActionsUtils'; +import {isCreatedTaskReportAction} from '@libs/ReportActionsUtils'; import {isOneTransactionReport} from '@libs/ReportUtils'; import {searchKeyToSavedSearchID} from '@libs/SearchKeyUtils'; import {buildCannedSearchQuery, buildSearchQueryString} from '@libs/SearchQueryUtils'; @@ -185,7 +183,6 @@ function Search({ const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { @@ -759,10 +756,7 @@ function Search({ } } - // A single-expense report in the table view opens at the same width as a multi-expense report. - // A "Pay someone" report always uses the expense view, so it keeps the normal wide width. - const isSentMoneyReport = isSentMoneyReportAction(firstTransaction?.reportAction); - if (shouldUseMultiExpenseReportLayout(item.transactions.length, shouldUseTableViewForSingleExpense && !isSentMoneyReport)) { + if (item.transactions.length > 1) { markReportRHPWidth(reportID, 'super-wide'); } else { unmarkReportRHPWidth(reportID, 'super-wide'); @@ -850,7 +844,6 @@ function Search({ getCurrencyDecimals, conciergeChat, delegateAccountID, - shouldUseTableViewForSingleExpense, ], ); diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts index 986657046e4c..0fb1ddd2d909 100644 --- a/src/hooks/useExpenseActions.ts +++ b/src/hooks/useExpenseActions.ts @@ -24,7 +24,6 @@ import { generateReportID, getPolicyExpenseChat, isDM, - isExpenseReport, isSelfDM, navigateOnDeleteExpense, } from '@libs/ReportUtils'; @@ -79,7 +78,6 @@ import {useAllPersonalDetails} from './usePersonalDetails'; import usePersonalPolicy from './usePersonalPolicy'; import useReportIsArchived from './useReportIsArchived'; import useRestrictedActionPolicyID from './useRestrictedActionPolicyID'; -import useSingleExpenseReportView from './useSingleExpenseReportView'; import useSplitEffectivePolicy from './useSplitEffectivePolicy'; import useTheme from './useTheme'; import useThrottledButtonState from './useThrottledButtonState'; @@ -212,12 +210,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic const hasMultipleSplits = !!transaction?.comment?.originalTransactionID && getChildTransactions(allTransactions, transaction.comment.originalTransactionID).length > 1; const hasSplitIndicator = isExpenseSplit && hasMultipleSplits; const isDeletingOwnExpense = requestParentReportAction?.actorAccountID === accountID; - - // In the table view a single-expense expense report is deleted as a report, so its expense moves to the self DM. - // IOU and invoice reports can't be deleted as a report, so they keep deleting their only expense. - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(reportID); - const shouldDeleteSingleExpense = isDeletingOwnExpense && !(shouldUseTableViewForSingleExpense && isExpenseReport(moneyRequestReport)); - const shouldShowEditSplitOnDeleteAction = shouldDeleteSingleExpense && !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]); + const shouldShowEditSplitOnDeleteAction = isDeletingOwnExpense && !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]); // Duplicate report throttle const [isDuplicateReportActive, temporarilyDisableDuplicateReportAction] = useThrottledButtonState(); @@ -557,7 +550,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic onSelected: async () => { const transactionCount = Object.keys(transactions).length; - if (transactionCount === 1 && shouldDeleteSingleExpense) { + if (transactionCount === 1 && isDeletingOwnExpense) { if (shouldShowEditSplitOnDeleteAction && transaction?.transactionID) { deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false); return; diff --git a/src/hooks/useSingleExpenseReportView.ts b/src/hooks/useSingleExpenseReportView.ts deleted file mode 100644 index e1d7af3f9af2..000000000000 --- a/src/hooks/useSingleExpenseReportView.ts +++ /dev/null @@ -1,41 +0,0 @@ -import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; - -import type {ValueOf} from 'type-fest'; - -import {isSentMoneyReportSelector} from '@selectors/ReportAction'; - -import useOnyx from './useOnyx'; - -type UseSingleExpenseReportViewResult = { - /** The view the user picked for single-expense reports, defaulting to the expense view */ - singleExpenseReportView: ValueOf; - - /** Whether single-expense reports should render in the table view */ - shouldUseTableViewForSingleExpense: boolean; - - /** Whether the saved preference is still loading from storage */ - isLoading: boolean; -}; - -/** - * Returns the user's saved view for single-expense reports (expense view or table view). - * - * Pass the report's ID to apply the view to that report. A "Pay someone" report always uses the expense view: its - * expense is the report itself, so it has no expense thread for a table row to open. - */ -function useSingleExpenseReportView(reportID?: string): UseSingleExpenseReportViewResult { - const [singleExpenseReportView = CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE, singleExpenseReportViewMetadata] = useOnyx(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW); - const [isSentMoneyReport = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(reportID)}`, {selector: isSentMoneyReportSelector}); - - return { - singleExpenseReportView, - shouldUseTableViewForSingleExpense: singleExpenseReportView === CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE && !isSentMoneyReport, - isLoading: isLoadingOnyxValue(singleExpenseReportViewMetadata), - }; -} - -export default useSingleExpenseReportView; diff --git a/src/hooks/useTransactionThreadReportID.ts b/src/hooks/useTransactionThreadReportID.ts index a4943a8d0c80..1ba28f4523eb 100644 --- a/src/hooks/useTransactionThreadReportID.ts +++ b/src/hooks/useTransactionThreadReportID.ts @@ -1,5 +1,5 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {getAllNonDeletedTransactions, getEffectiveTransactionThreadReportID} from '@libs/MoneyRequestReportUtils'; +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isSentMoneyReportAction} from '@libs/ReportActionsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -8,7 +8,6 @@ import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import usePaginatedReportActions from './usePaginatedReportActions'; import useReportTransactionsCollection from './useReportTransactionsCollection'; -import useSingleExpenseReportView from './useSingleExpenseReportView'; /** * Derives the single-transaction thread report IDs and filtered actions for a money request report. @@ -26,11 +25,10 @@ function useTransactionThreadReportID(reportID: string | undefined) { const nonDeletedTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true); const visibleTransactions = nonDeletedTransactions?.filter((t) => isOffline || t.pendingAction !== 'delete'); const reportTransactionIDs = visibleTransactions?.map((t) => t.transactionID); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(reportID); const transactionThreadReportID = getOneTransactionThreadReportID(moneyRequestReport, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); const isSentMoneyReport = reportActions.some((action) => isSentMoneyReportAction(action)); - const effectiveTransactionThreadReportID = getEffectiveTransactionThreadReportID(transactionThreadReportID, isSentMoneyReport, shouldUseTableViewForSingleExpense); + const effectiveTransactionThreadReportID = isSentMoneyReport ? undefined : transactionThreadReportID; return { transactionThreadReportID, diff --git a/src/languages/de.ts b/src/languages/de.ts index b266179670b7..8476c4631c45 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -10556,8 +10556,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc category: 'Kategorie', tag: 'Tag', }, - switchToTableView: 'Zur Tabellenansicht wechseln', - switchToExpenseView: 'Zur Ausgabenansicht wechseln', }, report: { newReport: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 91e95965f360..c6e0f81103a1 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -10756,8 +10756,6 @@ ${reportName}`, category: 'Κατηγορία', tag: 'Ετικέτα', }, - switchToTableView: 'Εναλλαγή σε προβολή πίνακα', - switchToExpenseView: 'Εναλλαγή σε προβολή δαπάνης', }, report: { newReport: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 62f0a9fde7b5..a61840a8eda9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -10800,8 +10800,6 @@ const translations = { category: 'Category', tag: 'Tag', }, - switchToTableView: 'Switch to table view', - switchToExpenseView: 'Switch to expense view', }, report: { newReport: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 3a2419adfe53..682a36c1bdb4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -10359,8 +10359,6 @@ ${reportName}`, category: 'Categoría', tag: 'Etiqueta', }, - switchToTableView: 'Cambiar a vista de tabla', - switchToExpenseView: 'Cambiar a vista de gasto', }, report: { newReport: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 82e02b34e51a..632e422b2f38 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -10582,8 +10582,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e category: 'Catégorie', tag: 'Tag', }, - switchToTableView: 'Passer à la vue tableau', - switchToExpenseView: 'Passer à la vue dépense', }, report: { newReport: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 8cc08900cd31..ff7ea314e915 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -10504,8 +10504,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, category: 'Categoria', tag: 'Etichetta', }, - switchToTableView: 'Passa alla vista tabella', - switchToExpenseView: 'Passa alla vista spesa', }, report: { newReport: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f40ef600826f..2835c29d0c3a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -10351,8 +10351,6 @@ ${reportName}`, category: 'カテゴリ', tag: 'タグ', }, - switchToTableView: 'テーブル表示に切り替え', - switchToExpenseView: '経費表示に切り替え', }, report: { newReport: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 56739889131c..3aa893043a1e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -10475,8 +10475,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, category: 'Categorie', tag: 'Label', }, - switchToTableView: 'Overschakelen naar tabelweergave', - switchToExpenseView: 'Overschakelen naar uitgavenweergave', }, report: { newReport: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7fbe47811243..e0f3bc9326dd 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -10491,8 +10491,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, category: 'Kategoria', tag: 'Tag', }, - switchToTableView: 'Przełącz na widok tabeli', - switchToExpenseView: 'Przełącz na widok wydatku', }, report: { newReport: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 374f235d7f7b..e43ff19bd211 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -10472,8 +10472,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, category: 'Categoria', tag: 'Etiqueta', }, - switchToTableView: 'Mudar para visualização em tabela', - switchToExpenseView: 'Mudar para visualização de despesa', }, report: { newReport: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 49d479d6153d..97a1d0fcae4e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -10076,8 +10076,6 @@ ${reportName}`, category: '类别', tag: '标签', }, - switchToTableView: '切换到表格视图', - switchToExpenseView: '切换到报销视图', }, report: { newReport: { diff --git a/src/libs/ExportOnyxState/masking.ts b/src/libs/ExportOnyxState/masking.ts index ca599c6cd7c1..a86f9fed81d5 100644 --- a/src/libs/ExportOnyxState/masking.ts +++ b/src/libs/ExportOnyxState/masking.ts @@ -324,7 +324,6 @@ const safeOnyxKeys = new Set([ ONYXKEYS.NVP_SEARCH_SIDEBAR, ONYXKEYS.NVP_SEEN_NEW_USER_MODAL, ONYXKEYS.NVP_SIDE_PANEL, - ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, ONYXKEYS.NVP_SUBMIT_MIGRATION_MODAL_SHOWN, ONYXKEYS.NVP_TRY_FOCUS_MODE, ONYXKEYS.NVP_TRY_NEW_DOT, diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts index 3724c72526c9..b3d1ee6f846a 100644 --- a/src/libs/MoneyRequestReportUtils.ts +++ b/src/libs/MoneyRequestReportUtils.ts @@ -166,30 +166,10 @@ function isSingleTransactionReport(report: OnyxEntry, transactions: Tran * Returns whether a "table" ReportView/MoneyRequestReportView should be used for the report. * * If report is a special "transaction thread" we want to use other Report views. - * Likewise, if report has only 1 connected transaction, then we also use other views, - * unless the user chose the table view for single-expense reports. + * Likewise, if report has only 1 connected transaction, then we also use other views. */ -function shouldDisplayReportTableView(report: OnyxEntry, transactions: Transaction[], shouldUseTableViewForSingleExpense = false) { - if (isReportTransactionThread(report)) { - return false; - } - return shouldUseTableViewForSingleExpense || !isSingleTransactionReport(report, transactions); -} - -/** - * Returns whether the report shows its expenses in a table and so gets the multi-expense layout (super-wide RHP). - * A single-expense report counts too when the user chose the table view for single-expense reports. - */ -function shouldUseMultiExpenseReportLayout(transactionCount: number, shouldUseTableViewForSingleExpense: boolean) { - return transactionCount > 1 || (shouldUseTableViewForSingleExpense && transactionCount === 1); -} - -/** - * Returns the transaction thread that report comments go to, or undefined when they go to the report itself. - * In the table view the thread's actions aren't shown in the report, so comments go to the report itself. - */ -function getEffectiveTransactionThreadReportID(transactionThreadReportID: string | undefined, isSentMoneyReport: boolean, shouldUseTableViewForSingleExpense: boolean) { - return isSentMoneyReport || shouldUseTableViewForSingleExpense ? undefined : transactionThreadReportID; +function shouldDisplayReportTableView(report: OnyxEntry, transactions: Transaction[]) { + return !isReportTransactionThread(report) && !isSingleTransactionReport(report, transactions); } function shouldWaitForTransactions( @@ -273,8 +253,6 @@ export { getAllNonDeletedTransactions, isSingleTransactionReport, shouldDisplayReportTableView, - shouldUseMultiExpenseReportLayout, - getEffectiveTransactionThreadReportID, shouldWaitForTransactions, isBillableEnabledOnPolicy, getTransactionRejectErrorKey, diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 600fa9153349..271f69728542 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -14,7 +14,6 @@ import usePersonalDetailByLogin from '@hooks/usePersonalDetailByLogin'; import useReconcileHighContrastIntent from '@hooks/useReconcileHighContrastIntent'; import useReportAttributes from '@hooks/useReportAttributes'; import useRootNavigationState from '@hooks/useRootNavigationState'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import {init, isClientTheLeader} from '@libs/ActiveClientManager'; import {isQAServerActive} from '@libs/ApiUtils'; @@ -123,10 +122,7 @@ function AuthScreensInitHandler() { const topmostReportID = useRootNavigationState(Navigation.getFocusedReportId); // Not `Navigation.isNavigationReady()`: that resolves on the container's first render, before this navigator exists. const tabNavigatorStateKey = useRootNavigationState(getTabNavigatorStateKey); - const oneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(topmostReportID); - // The table view doesn't show the expense thread's actions, so a comment there should still notify while the report is open - const topmostOneTransactionThreadReportID = shouldUseTableViewForSingleExpense ? undefined : oneTransactionThreadReportID; + const topmostOneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID); // We use a ref so the Pusher callback (registered once on mount) always reads the latest value without re-subscribing. const topmostOneTransactionThreadReportIDRef = useRef(topmostOneTransactionThreadReportID); diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index ce23ffb29146..2c3631b9b7cc 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -10,8 +10,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Report as ReportType, ReportActions} from '@src/types/onyx'; import type {PushPayload} from '@ua/react-native-airship'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -39,14 +38,6 @@ Onyx.connectWithoutView({ callback: (value) => (allReports = value), }); -// connectWithoutView() is the only option here: Airship calls shouldShowPushNotification() from outside the React tree and owns its callback signature, -// so we can neither use useOnyx() nor pass this value in as a parameter. This is not a pattern to copy - prefer useOnyx() or passing data in as a parameter. -let singleExpenseReportView: OnyxEntry>; -Onyx.connectWithoutView({ - key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, - callback: (value) => (singleExpenseReportView = value), -}); - /** * Returns whether the given Airship notification should be shown depending on the current state of the app */ @@ -67,12 +58,7 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; const topmostChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; const topmostReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReportID}`]; - // The table view doesn't show the expense thread's actions, so a comment there should still notify while the report is open. A "Pay someone" report always uses the expense view. - const isSentMoneyReport = Object.values(topmostReportActions ?? {}).some(ReportActionUtils.isSentMoneyReportAction); - const shouldUseTableViewForSingleExpense = singleExpenseReportView === CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE && !isSentMoneyReport; - const topmostOneTransactionThreadReportID = shouldUseTableViewForSingleExpense - ? undefined - : ReportActionUtils.getOneTransactionThreadReportID(topmostReport, topmostChatReport, topmostReportActions, getIsOffline()); + const topmostOneTransactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(topmostReport, topmostChatReport, topmostReportActions, getIsOffline()); shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), topmostOneTransactionThreadReportID, currentUserAccountID, reportAction, true); } diff --git a/src/libs/PopoverMenuSections.ts b/src/libs/PopoverMenuSections.ts index cd78b91fe32e..41d4ec38e1f4 100644 --- a/src/libs/PopoverMenuSections.ts +++ b/src/libs/PopoverMenuSections.ts @@ -24,7 +24,7 @@ const REPORT_MORE_MENU_SECTIONS = [ [REPORT_SECONDARY_ACTIONS.SPLIT, REPORT_SECONDARY_ACTIONS.MERGE, REPORT_SECONDARY_ACTIONS.DUPLICATE_EXPENSE, REPORT_SECONDARY_ACTIONS.MOVE_EXPENSE], [REPORT_SECONDARY_ACTIONS.DUPLICATE_REPORT, REPORT_SECONDARY_ACTIONS.CHANGE_WORKSPACE, REPORT_SECONDARY_ACTIONS.CHANGE_APPROVER], [REPORT_SECONDARY_ACTIONS.EXPORT, REPORT_SECONDARY_ACTIONS.DOWNLOAD_PDF, REPORT_SECONDARY_ACTIONS.DOWNLOAD_RECEIPTS, REPORT_SECONDARY_ACTIONS.SHARE, REPORT_SECONDARY_ACTIONS.PRINT], - [REPORT_SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW, REPORT_SECONDARY_ACTIONS.VIEW_DETAILS, REPORT_SECONDARY_ACTIONS.DELETE], + [REPORT_SECONDARY_ACTIONS.VIEW_DETAILS, REPORT_SECONDARY_ACTIONS.DELETE], ]; const TRANSACTION_MORE_MENU_SECTIONS = [ diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 72df9606c94f..9025500c1e9f 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -19,7 +19,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import {areTransactionsEligibleForMerge} from './MergeTransactionUtils'; -import {isSingleTransactionReport} from './MoneyRequestReportUtils'; import {getLoginByAccountID} from './PersonalDetailsUtils'; import { arePaymentsEnabled as arePaymentsEnabledUtils, @@ -52,7 +51,6 @@ import { hasPendingDEWApprove, hasPendingDEWSubmit, isPayAction, - isSentMoneyReportAction, } from './ReportActionsUtils'; import {getReportPrimaryAction, isPrimaryPayAction} from './ReportPrimaryActionUtils'; import { @@ -1229,12 +1227,6 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER); } - // Reports with more than one expense always use the table view, so the switch only applies to single-expense reports. - // "Pay someone" reports always use the expense view, because their expense is the report itself. - if (isSingleTransactionReport(report, reportTransactions) && !reportActions?.some(isSentMoneyReportAction)) { - options.push(CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW); - } - options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS); if (isDeleteAction(report, reportTransactions, currentUserAccountID, rules, submitterLogin, policy, cardList, reportActions ?? [], true)) { diff --git a/src/libs/TransactionUtils/duplicates.ts b/src/libs/TransactionUtils/duplicates.ts index 92a11d42aad6..b3ec2c66ea5c 100644 --- a/src/libs/TransactionUtils/duplicates.ts +++ b/src/libs/TransactionUtils/duplicates.ts @@ -221,17 +221,6 @@ function canMergeDuplicates(keptReport: OnyxEntry, transactionIDList: st return isReportMergeableForDuplicates(keptReport) && transactionIDList.length > 0; } -/** - * Whether a report still has an expense after a duplicate merge discards the given transactions. Expenses that are - * already pending deletion don't count. A report whose transactions haven't loaded is treated as keeping expenses. - */ -function doesReportKeepExpensesAfterMerge(reportTransactions: Transaction[], discardedTransactionIDs: Set): boolean { - if (reportTransactions.length === 0) { - return true; - } - return reportTransactions.some((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !discardedTransactionIDs.has(transaction.transactionID)); -} - /** * This function compares fields of duplicate transactions and determines which fields should be kept and which should be changed. * @@ -481,7 +470,6 @@ export { buildNewTransactionAfterReviewingDuplicates, canMergeDuplicates, compareDuplicateTransactionFields, - doesReportKeepExpensesAfterMerge, removeSettledAndApprovedTransactions, removeTransactionFromDuplicateTransactionViolation, }; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 83bcd1373d81..78ca4988b96b 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -53,7 +53,6 @@ import { buildNewTransactionAfterReviewingDuplicates, canMergeDuplicates, compareDuplicateTransactionFields, - doesReportKeepExpensesAfterMerge, removeSettledAndApprovedTransactions, removeTransactionFromDuplicateTransactionViolation, } from './duplicates'; @@ -1942,7 +1941,6 @@ export { buildNewTransactionAfterReviewingDuplicates, buildMergeDuplicatesParams, canMergeDuplicates, - doesReportKeepExpensesAfterMerge, getReimbursable, hasNonReimbursableTransactions, isPayAtEndExpense, diff --git a/src/libs/actions/ReportLayout.ts b/src/libs/actions/ReportLayout.ts index 30740eb725c8..dedc156894a4 100644 --- a/src/libs/actions/ReportLayout.ts +++ b/src/libs/actions/ReportLayout.ts @@ -10,14 +10,9 @@ import type {ReportLayoutGroupBy, ReportLayoutOption, ReportLayoutSelection} fro import type {AnyOnyxUpdate} from '@src/types/onyx/Request'; import type {OnyxUpdate} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import Onyx from 'react-native-onyx'; -type SingleExpenseReportView = ValueOf; - -const SINGLE_EXPENSE_REPORT_VIEW_NVP_NAME = 'expensify_singleExpenseReportView'; - /** * Apply a report layout selection from the group-by selector. * "None" maps to the matrix layout (a flat, ungrouped list) and clears the group-by field. @@ -142,32 +137,4 @@ function setReportDetailsColumns(columns: SearchCustomColumnIds[], previousValue API.write(WRITE_COMMANDS.SET_REPORT_DETAILS_COLUMNS, parameters, {optimisticData, failureData}); } -/** - * Set whether the user views single-expense reports in the expense view or the table view. - * The preference is saved on the account so it applies to every single-expense report on every device. - */ -function setSingleExpenseReportView(view: SingleExpenseReportView, previousValue?: SingleExpenseReportView | null) { - const optimisticData: Array> = [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, - value: view, - }, - ]; - - const failureData: Array> = [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, - value: previousValue ?? null, - }, - ]; - - const parameters: SetNameValuePairsParams = { - [`nameValuePairs[${SINGLE_EXPENSE_REPORT_VIEW_NVP_NAME}]`]: view, - }; - - API.write(WRITE_COMMANDS.SET_NAME_VALUE_PAIRS, parameters, {optimisticData, failureData}); -} - -export {setReportLayout, getReportLayoutGroupBy, getReportLayoutSelection, isMatrixLayout, setReportDetailsColumns, setSingleExpenseReportView}; +export {setReportLayout, getReportLayoutGroupBy, getReportLayoutSelection, isMatrixLayout, setReportDetailsColumns}; diff --git a/src/pages/DynamicReportDetailsPage/hooks/useReportDetailsDeleteModal.ts b/src/pages/DynamicReportDetailsPage/hooks/useReportDetailsDeleteModal.ts index a866b933ac93..d07aee9cd73b 100644 --- a/src/pages/DynamicReportDetailsPage/hooks/useReportDetailsDeleteModal.ts +++ b/src/pages/DynamicReportDetailsPage/hooks/useReportDetailsDeleteModal.ts @@ -5,7 +5,6 @@ import useConfirmModal from '@hooks/useConfirmModal'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useLocalize from '@hooks/useLocalize'; -import getCentralPaneReportID from '@libs/Navigation/helpers/getCentralPaneReportID'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {isTrackExpenseAction} from '@libs/ReportActionsUtils'; @@ -48,7 +47,6 @@ function useReportDetailsDeleteModal(reportID: string, caseID: CaseID, parentRep moneyRequestReport, iouTransactionID, iouReport, - iouReportTransactions, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView, @@ -56,9 +54,6 @@ function useReportDetailsDeleteModal(reportID: string, caseID: CaseID, parentRep }: Partial = requestData ?? {}; let urlToNavigateBack: Route | undefined; - // Deleting a report's only expense deletes the report too. A single-expense report in the table view opens its - // expense on top of the report, so going back to the report would land on the "Not here" page. - let shouldCloseRHPAfterReportDeletion = false; // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { const rootState = navigationRef.getRootState(); @@ -67,24 +62,17 @@ function useReportDetailsDeleteModal(reportID: string, caseID: CaseID, parentRep const previousRoute = rhpRoutes.at(-2); const previousRouteReportID = previousRoute?.params && 'reportID' in previousRoute.params ? previousRoute.params.reportID : undefined; const superWideRHPIndex = rhpRoutes.findIndex((rhpRoute) => SUPER_WIDE_RIGHT_MODALS.has(rhpRoute.name)); - const superWideRHPParams = rhpRoutes.at(superWideRHPIndex)?.params; - const superWideRHPReportID = superWideRHPIndex > -1 && superWideRHPParams && 'reportID' in superWideRHPParams ? superWideRHPParams.reportID : undefined; - const remainingTransactions = iouReportTransactions?.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const isParentReportDeleted = !!iouReport?.reportID && remainingTransactions?.length === 1; // If the deleted expense is displayed directly below, close the entire RHP const isSuperWideRHPDisplayed = superWideRHPIndex > -1; - shouldCloseRHPAfterReportDeletion = isParentReportDeleted && superWideRHPReportID === iouReport?.reportID; - // On narrow layouts the report sits in the Inbox central pane instead of the super wide RHP - const isDeletedReportUnderRHP = shouldCloseRHPAfterReportDeletion || (isParentReportDeleted && getCentralPaneReportID() === iouReport?.reportID); const isSuperWideRHPDisplayedDirectlyBelow = isSuperWideRHPDisplayed && superWideRHPIndex === rhpRoutes.length - 2; if (isSuperWideRHPDisplayedDirectlyBelow && previousRouteReportID === reportID) { Navigation.dismissModal(); return; } - // If the deleted expense is opened from the super wide rhp, go back there, unless going back would show the deleted report. - if (!isDeletedReportUnderRHP && previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && previousRouteReportID === reportID) { + // If the deleted expense is opened from the super wide rhp, go back there. + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && previousRouteReportID === reportID) { if (isSuperWideRHPDisplayed) { const distanceToPop = rhpRoutes.length - 1 - superWideRHPIndex; navigationRef.dispatch({...StackActions.pop(distanceToPop), target: rhp?.state?.key}); @@ -123,10 +111,6 @@ function useReportDetailsDeleteModal(reportID: string, caseID: CaseID, parentRep if (!urlToNavigateBack) { Navigation.dismissModal(); - } else if (shouldCloseRHPAfterReportDeletion) { - // Going back to the super wide RHP would show the deleted report, so close the RHP to reveal the screen below - setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - Navigation.dismissModal(); } else { setDeleteTransactionNavigateBackUrl(urlToNavigateBack); navigateBackOnDeleteTransaction(urlToNavigateBack); diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 84e34f35d446..f5cdbe80163c 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -18,7 +18,6 @@ import usePrevious from '@hooks/usePrevious'; import {useDerivedReportNameByReportID} from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useSubmitToDestinationVisible from '@hooks/useSubmitToDestinationVisible'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; @@ -26,7 +25,7 @@ import useTransactionThreadReportID from '@hooks/useTransactionThreadReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Log from '@libs/Log'; -import {getAllNonDeletedTransactions, shouldUseMultiExpenseReportLayout} from '@libs/MoneyRequestReportUtils'; +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; @@ -127,7 +126,6 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(report?.reportID); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); @@ -186,9 +184,8 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return {snapshotTransaction: transaction, snapshotViolations: violations}; }, [snapshot?.data, allReportTransactions, reportIDFromRoute]); - // If there is more than one transaction, display the report in Super Wide RHP, otherwise it will be shown in Wide RHP. - // A single-expense report in the table view also uses the Super Wide RHP. - const shouldShowSuperWideRHP = shouldUseMultiExpenseReportLayout(visibleTransactions.length, shouldUseTableViewForSingleExpense); + // If there is more than one transaction, display the report in Super Wide RHP, otherwise it will be shown in Wide RHP + const shouldShowSuperWideRHP = visibleTransactions.length > 1; useRHPWidth(shouldShowSuperWideRHP ? 'super-wide' : 'wide'); diff --git a/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx b/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx index fdc5ff3569ac..cc6e82235d9f 100644 --- a/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx +++ b/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx @@ -16,7 +16,6 @@ import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsByID from '@hooks/useTransactionsByID'; import useTransactionThreadReportIDs from '@hooks/useTransactionThreadReportIDs'; @@ -74,8 +73,6 @@ function DynamicConfirmationPage() { ); const {superWideRHPRouteKeys} = useWideRHPState(); const isSuperWideRHPDisplayed = superWideRHPRouteKeys.length > 0; - const superWideRHPReportID = isSuperWideRHPDisplayed ? Navigation.getTopmostSuperWideRHPReportID() : undefined; - const superWideRHPReportTransactions = useReportTransactionsCollection(superWideRHPReportID); const [duplicates] = useTransactionsByID(reviewDuplicates?.duplicates); const transactionsMergeParams = useMemo( @@ -125,11 +122,7 @@ function DynamicConfirmationPage() { allReportActionsList: allReportActions, allReportsList: allReports, }); - // The merge removes the discarded expenses. A single-expense report in the table view also uses the super wide RHP, - // and once its only expense is discarded it is gone, so going back to it would show "Not here". - const discardedTransactionIDs = new Set(mergeParams.transactionList.map((transaction) => transaction.transactionID)); - const doesSuperWideRHPReportKeepExpenses = TransactionUtils.doesReportKeepExpensesAfterMerge(Object.values(superWideRHPReportTransactions), discardedTransactionIDs); - if (isSuperWideRHPDisplayed && doesSuperWideRHPReportKeepExpenses) { + if (isSuperWideRHPDisplayed) { Navigation.dismissToSuperWideRHP(); return; } @@ -142,18 +135,7 @@ function DynamicConfirmationPage() { Navigation.dismissModal({ afterTransition: () => Navigation.navigate(keptReportRoute, {forceReplace: true}), }); - }, [ - childReportID, - transactionsMergeParams, - taxData, - currentUserAccountID, - currentUserLogin, - isSuperWideRHPDisplayed, - superWideRHPReportTransactions, - allTransactionViolations, - allReportActions, - allReports, - ]); + }, [childReportID, transactionsMergeParams, taxData, currentUserAccountID, currentUserLogin, isSuperWideRHPDisplayed, allTransactionViolations, allReportActions, allReports]); const handleResolveDuplicates = useCallback(() => { resolveDuplicates({...transactionsMergeParams, ...taxData, transactionThreadReportIDMap, allTransactionViolations, allReportActionsList: allReportActions, delegateAccountID}); diff --git a/src/pages/inbox/OneTransactionThreadRedirectHandler.tsx b/src/pages/inbox/OneTransactionThreadRedirectHandler.tsx index b1474000b33c..5dd4288127e8 100644 --- a/src/pages/inbox/OneTransactionThreadRedirectHandler.tsx +++ b/src/pages/inbox/OneTransactionThreadRedirectHandler.tsx @@ -1,6 +1,5 @@ import useIsOneTransactionThread from '@hooks/useIsOneTransactionThread'; import useOnyx from '@hooks/useOnyx'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; @@ -86,15 +85,10 @@ function OneTransactionThreadRedirectHandler() { // A message deep link points at an action inside the thread, so dropping the thread route would drop its anchor. const hasLinkedReportAction = !!route.params?.reportActionID; - // In the table view the expense is a row in the report, so its thread is a real destination. Wait for the saved - // preference to load so a deep link doesn't redirect before we know which view the user picked. - const {shouldUseTableViewForSingleExpense, isLoading: isSingleExpenseReportViewLoading} = useSingleExpenseReportView(); - const referrer = route.name === SCREENS.REPORT ? route.params?.referrer : undefined; const backTo = route.params?.backTo; - const shouldRedirectToParentReport = - !!parentReportID && isOneTransactionThread && !isSteppingThroughExpenses && !hasLinkedReportAction && !isSingleExpenseReportViewLoading && !shouldUseTableViewForSingleExpense; + const shouldRedirectToParentReport = !!parentReportID && isOneTransactionThread && !isSteppingThroughExpenses && !hasLinkedReportAction; useEffect(() => { if (!isFocused || !shouldRedirectToParentReport || !parentReportID) { diff --git a/src/pages/inbox/ReportActions.tsx b/src/pages/inbox/ReportActions.tsx index 035e9eee7d83..e864155f7bc3 100644 --- a/src/pages/inbox/ReportActions.tsx +++ b/src/pages/inbox/ReportActions.tsx @@ -6,7 +6,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getAllNonDeletedTransactions, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils'; @@ -55,11 +54,10 @@ function ReportActions() { const allReportTransactions = useReportTransactionsCollection(reportIDFromRoute); const reportTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(report?.reportID); const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, reportTransactions, reportLoadingState, isReportLoadPending, isOffline); - const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, reportTransactions, shouldUseTableViewForSingleExpense); + const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, reportTransactions); // The app-load skeleton is hoisted out of the body so the body's data hooks/effects never run // during app boot. It only applies on the chat path (after the skeleton and money-request diff --git a/src/pages/inbox/WideRHPReceiptPanel.tsx b/src/pages/inbox/WideRHPReceiptPanel.tsx index fa5b5df087c1..1e6f3d200a34 100644 --- a/src/pages/inbox/WideRHPReceiptPanel.tsx +++ b/src/pages/inbox/WideRHPReceiptPanel.tsx @@ -9,11 +9,10 @@ import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParentReportAction from '@hooks/useParentReportAction'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {getAllNonDeletedTransactions, shouldDisplayReportTableView, shouldUseMultiExpenseReportLayout} from '@libs/MoneyRequestReportUtils'; +import {getAllNonDeletedTransactions, shouldDisplayReportTableView} from '@libs/MoneyRequestReportUtils'; import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isTransactionThread} from '@libs/ReportActionsUtils'; import {isInvoiceReport, isMoneyRequestReport} from '@libs/ReportUtils'; @@ -62,14 +61,10 @@ function WideRHPReceiptPanelGate() { const reportTransactionIDs = visibleTransactions?.map((transaction) => transaction.transactionID); const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(report?.reportID); const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); - const visibleTransactionsCount = visibleTransactions?.length ?? 0; - // A single-expense report in the table view gets the same layout as a multi-expense report. Empty reports keep the wide layout. - const hasTransactionsInTable = shouldUseMultiExpenseReportLayout(visibleTransactionsCount, shouldUseTableViewForSingleExpense); - const isConfirmedMultiTransactionReport = - isMoneyRequestOrInvoiceReport && hasTransactionsInTable && shouldDisplayReportTableView(report, visibleTransactions ?? [], shouldUseTableViewForSingleExpense); + const hasMultipleTransactions = (visibleTransactions?.length ?? 0) > 1; + const isConfirmedMultiTransactionReport = isMoneyRequestOrInvoiceReport && hasMultipleTransactions && shouldDisplayReportTableView(report, visibleTransactions ?? []); const shouldShowWideRHP = !isConfirmedMultiTransactionReport && diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts b/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts index 9c6a51e1bf50..4bff522a7cdc 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts @@ -2,9 +2,8 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; -import {getAllNonDeletedTransactions, getEffectiveTransactionThreadReportID} from '@libs/MoneyRequestReportUtils'; +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isSentMoneyReportAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; @@ -23,7 +22,6 @@ function useComposerReportData(reportID: string): ComposerReportData { const {isOffline} = useNetwork(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`); - const {shouldUseTableViewForSingleExpense} = useSingleExpenseReportView(reportID); const {reportActions: unfilteredReportActions} = usePaginatedReportActions(report?.reportID); const filteredReportActions = getFilteredReportActionsForReportView(unfilteredReportActions); const allReportTransactions = useReportTransactionsCollection(reportID); @@ -32,7 +30,7 @@ function useComposerReportData(reportID: string): ComposerReportData { const reportTransactionIDs = visibleTransactions?.map((t) => t.transactionID); const isSentMoneyReport = filteredReportActions.some((action) => isSentMoneyReportAction(action)); const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, filteredReportActions, isOffline, reportTransactionIDs); - const effectiveTransactionThreadReportID = getEffectiveTransactionThreadReportID(transactionThreadReportID, isSentMoneyReport, shouldUseTableViewForSingleExpense); + const effectiveTransactionThreadReportID = isSentMoneyReport ? undefined : transactionThreadReportID; return {report, filteredReportActions, effectiveTransactionThreadReportID}; } diff --git a/src/selectors/ReportAction.ts b/src/selectors/ReportAction.ts index 10aa9dbee76c..c27d0c5824a4 100644 --- a/src/selectors/ReportAction.ts +++ b/src/selectors/ReportAction.ts @@ -6,7 +6,6 @@ import { getOriginalMessage, getSortedReportActions, isActionOfType, - isSentMoneyReportAction, } from '@libs/ReportActionsUtils'; import {hasReportBeenForwardedSinceLastSubmit} from '@libs/ReportUtils'; @@ -153,9 +152,6 @@ function getReceiptScanFailedIOUActionDataSelector( const transactionThreadReportIDSelector = (transactionID: string | undefined) => (reportActions: OnyxEntry) => transactionID ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transactionID)?.childReportID : undefined; -/** Returns whether the report is a "Pay someone" (sent money) report, which has a pay action with IOUDetails instead of an expense-creating one. */ -const isSentMoneyReportSelector = (reportActions: OnyxEntry) => Object.values(reportActions ?? {}).some(isSentMoneyReportAction); - /** Returns the ID of the author's newest comment in the report that can show the feedback prompt. */ const getLatestFeedbackActionIDByAuthorSelector = (authorAccountID: number | undefined) => (reportActions: OnyxEntry) => authorAccountID ? getLatestConciergeFeedbackActionIDFromReportActions(reportActions, authorAccountID) : undefined; @@ -191,7 +187,6 @@ function getParentReportActionWithoutThreadRepliesSelector(parentReportActions: } export { - isSentMoneyReportSelector, getLatestFeedbackActionIDByAuthorSelector, getParentReportActionSelector, getLastClosedReportAction, diff --git a/tests/actions/ReportLayoutTest.ts b/tests/actions/ReportLayoutTest.ts index 0614efcd5b55..beb19dcd957b 100644 --- a/tests/actions/ReportLayoutTest.ts +++ b/tests/actions/ReportLayoutTest.ts @@ -1,9 +1,8 @@ -import {getReportLayoutGroupBy, getReportLayoutSelection, isMatrixLayout, setReportLayout, setSingleExpenseReportView} from '@libs/actions/ReportLayout'; +import {getReportLayoutGroupBy, getReportLayoutSelection, isMatrixLayout, setReportLayout} from '@libs/actions/ReportLayout'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; jest.mock('@libs/API'); @@ -11,7 +10,6 @@ const mockWrite = jest.mocked(API.write); const LAYOUT_OPTION_NVP_NAME = 'expensify_layoutOption'; const GROUP_BY_OPTION_NVP_NAME = 'expensify_groupByOption'; -const SINGLE_EXPENSE_REPORT_VIEW_NVP_NAME = 'expensify_singleExpenseReportView'; const NAME_VALUE_PAIRS_KEY_PREFIX = 'nameValuePairs['; function isRecord(value: unknown): value is Record { @@ -150,40 +148,3 @@ describe('getReportLayoutSelection', () => { expect(getReportLayoutSelection(undefined, undefined)).toBe(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY); }); }); - -describe('setSingleExpenseReportView', () => { - beforeEach(() => { - mockWrite.mockClear(); - }); - - it('saves the table view on the account NVP', () => { - // Given a user who hasn't picked a view yet - // When they switch to the table view - setSingleExpenseReportView(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - - // Then the choice is saved on their account so it applies on every device - expect(mockWrite).toHaveBeenCalledTimes(1); - expect(mockWrite.mock.calls.at(0)?.[0]).toBe(WRITE_COMMANDS.SET_NAME_VALUE_PAIRS); - expect(getWrittenNameValuePairs()).toEqual({[SINGLE_EXPENSE_REPORT_VIEW_NVP_NAME]: CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE}); - }); - - it('applies the new view optimistically and restores the previous view on failure', () => { - // Given a user who is on the table view - // When they switch back to the expense view - setSingleExpenseReportView(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - - // Then the expense view shows right away, and the table view comes back if the request fails - const onyxData = mockWrite.mock.calls.at(0)?.[2]; - expect(onyxData?.optimisticData).toEqual([{onyxMethod: 'set', key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, value: CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE}]); - expect(onyxData?.failureData).toEqual([{onyxMethod: 'set', key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, value: CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE}]); - }); - - it('clears the optimistic view on failure when no view was saved before', () => { - // Given a user who hasn't picked a view yet - // When they switch to the table view - setSingleExpenseReportView(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - - // Then a failed request falls back to the default view - expect(mockWrite.mock.calls.at(0)?.[2]?.failureData).toEqual([{onyxMethod: 'set', key: ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, value: null}]); - }); -}); diff --git a/tests/navigation/OneTransactionThreadRedirectHandlerTest.tsx b/tests/navigation/OneTransactionThreadRedirectHandlerTest.tsx index 6ba8877286d3..ab13e3d23aee 100644 --- a/tests/navigation/OneTransactionThreadRedirectHandlerTest.tsx +++ b/tests/navigation/OneTransactionThreadRedirectHandlerTest.tsx @@ -20,7 +20,6 @@ const SIBLING_TRANSACTION_ID = '22222'; // Inlined because a `jest.mock` factory may only close over locals initialized with a literal. const TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS_KEY = 'transactionThreadNavigationTransactionIDs'; -const SINGLE_EXPENSE_REPORT_VIEW_KEY = 'nvp_expensify_singleExpenseReportView'; const mockNavigate = jest.fn(); @@ -54,8 +53,6 @@ jest.mock('@react-navigation/native', () => { let mockParentReportID: string | undefined = EXPENSE_REPORT_ID; let mockParentTransactionCount: number | undefined = 1; let mockSiblingTransactionIDs: string[] | undefined; -let mockSingleExpenseReportView: string | undefined; -let mockSingleExpenseReportViewStatus = 'loaded'; jest.mock('@hooks/useOnyx', () => ({ __esModule: true, @@ -63,9 +60,6 @@ jest.mock('@hooks/useOnyx', () => ({ if (key === TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS_KEY) { return [options?.selector ? options.selector(mockSiblingTransactionIDs) : mockSiblingTransactionIDs, {status: 'loaded'}]; } - if (key === SINGLE_EXPENSE_REPORT_VIEW_KEY) { - return [mockSingleExpenseReportView, {status: mockSingleExpenseReportViewStatus}]; - } const value = key.endsWith(EXPENSE_REPORT_ID) ? {reportID: EXPENSE_REPORT_ID, type: 'expense', transactionCount: mockParentTransactionCount} : {reportID: THREAD_REPORT_ID, parentReportID: mockParentReportID, parentReportActionID: '1'}; @@ -108,8 +102,6 @@ describe('OneTransactionThreadRedirectHandler', () => { mockOneTransactionThreadReportID = THREAD_REPORT_ID; mockParentReportAction = createIOUAction(CONST.IOU.REPORT_ACTION_TYPE.CREATE); mockSiblingTransactionIDs = undefined; - mockSingleExpenseReportView = undefined; - mockSingleExpenseReportViewStatus = 'loaded'; }); it('replaces the route with the parent report when the thread is the only expense of the report', async () => { @@ -297,46 +289,4 @@ describe('OneTransactionThreadRedirectHandler', () => { expect(mockNavigate).not.toHaveBeenCalled(); }); - - it('mocks the single-expense report view under the key the handler subscribes to', () => { - expect(SINGLE_EXPENSE_REPORT_VIEW_KEY).toBe(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW); - }); - - it('keeps the thread route when the user views single-expense reports in the table view', async () => { - // Given the user picked the table view, where the expense is a row the user opens on purpose - mockSingleExpenseReportView = CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE; - - // When the expense's thread opens - render(); - - await waitForBatchedUpdatesWithAct(); - - // Then it stays on the thread instead of bouncing back to the report - expect(mockNavigate).not.toHaveBeenCalled(); - }); - - it('waits for the single-expense report view to load before redirecting', async () => { - // Given the saved view hasn't loaded yet, so we can't tell whether the user picked the table view - mockSingleExpenseReportViewStatus = 'loading'; - - // When the expense's thread opens, for example from a deep link - render(); - - await waitForBatchedUpdatesWithAct(); - - // Then it doesn't redirect too early - expect(mockNavigate).not.toHaveBeenCalled(); - }); - - it('redirects when the user views single-expense reports in the expense view', async () => { - // Given the user explicitly picked the expense view - mockSingleExpenseReportView = CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE; - - // When the expense's thread opens - render(); - - // Then it's replaced with the report, which shows the expense inline - await waitFor(() => expect(mockNavigate).toHaveBeenCalledTimes(1)); - expect(mockNavigate).toHaveBeenCalledWith(`r/${EXPENSE_REPORT_ID}`, {forceReplace: true}); - }); }); diff --git a/tests/ui/AuthScreensInitHandlerTest.tsx b/tests/ui/AuthScreensInitHandlerTest.tsx index 2025d0dc4795..bf024aebff8c 100644 --- a/tests/ui/AuthScreensInitHandlerTest.tsx +++ b/tests/ui/AuthScreensInitHandlerTest.tsx @@ -2,8 +2,6 @@ import {render} from '@testing-library/react-native'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import useOneTransactionThreadReportID from '@hooks/useOneTransactionThreadReportID'; - import {init as activeClientManagerInit, isClientTheLeader, isReady} from '@libs/ActiveClientManager'; import {isQAServerActive} from '@libs/ApiUtils'; import AuthScreensInitHandler from '@libs/Navigation/AppNavigator/AuthScreensInitHandler'; @@ -34,7 +32,6 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch const TEST_ACCOUNT_ID = 1; const QA_APP_KEY = 'qa-app-key'; -const TRANSACTION_THREAD_REPORT_ID = '20'; jest.mock('@libs/ApiUtils', () => ({ ...jest.requireActual>('@libs/ApiUtils'), @@ -129,11 +126,6 @@ jest.mock('@selectors/Onboarding', () => ({ jest.mock('@src/components/ConfirmedRoute.tsx'); -jest.mock('@hooks/useOneTransactionThreadReportID', () => ({ - __esModule: true, - default: jest.fn(() => undefined), -})); - const mockedPusherInit = jest.mocked(Pusher.init); const mockedGetCurrentUrl = jest.mocked(getCurrentUrl); const mockedIsActiveRoute = jest.mocked(Navigation.isActiveRoute); @@ -143,7 +135,6 @@ const mockedIsClientTheLeader = jest.mocked(isClientTheLeader); const mockedIsReady = jest.mocked(isReady); const mockedSubscribeToUserEvents = jest.mocked(subscribeToUserEvents); const mockedIsQAServerActive = jest.mocked(isQAServerActive); -const mockedUseOneTransactionThreadReportID = jest.mocked(useOneTransactionThreadReportID); function renderAuthScreensInitHandler() { return render( @@ -170,7 +161,6 @@ describe('AuthScreensInitHandler', () => { mockedIsReady.mockReturnValue(Promise.resolve()); mockedIsActiveRoute.mockReturnValue(false); mockedIsQAServerActive.mockReturnValue(false); - mockedUseOneTransactionThreadReportID.mockReturnValue(undefined); wrapOnyxWithWaitForBatchedUpdates(Onyx); await Onyx.clear(); await waitForBatchedUpdates(); @@ -245,25 +235,6 @@ describe('AuthScreensInitHandler', () => { expect(getter()).toBeUndefined(); }); - it.each([ - ['expense', CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE, TRANSACTION_THREAD_REPORT_ID], - ['table', CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE, undefined], - ])('passes the right expense thread to Pusher notifications when single-expense reports use the %s view', async (_view, singleExpenseReportView, expectedThreadReportID) => { - // Given an open single-expense report with an expense thread. Only the expense view shows that thread's comments. - mockedUseOneTransactionThreadReportID.mockReturnValue(TRANSACTION_THREAD_REPORT_ID); - await Onyx.merge(ONYXKEYS.SESSION, {accountID: TEST_ACCOUNT_ID, email: 'test@test.com'}); - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, singleExpenseReportView); - await waitForBatchedUpdates(); - - // When the handler subscribes to user events - renderAuthScreensInitHandler(); - await waitForBatchedUpdatesWithAct(); - - // Then the thread getter returns the thread only when its comments are shown, so in the table view they still notify - const getTopmostOneTransactionThreadReportID = mockedSubscribeToUserEvents.mock.calls.at(0)?.[2]; - expect(getTopmostOneTransactionThreadReportID?.()).toBe(expectedThreadReportID); - }); - it.each([ ['production', false, CONFIG.PUSHER.APP_KEY], ['QA', true, QA_APP_KEY], diff --git a/tests/ui/components/MoneyReportHeaderActionsSingleExpenseTableViewTest.tsx b/tests/ui/components/MoneyReportHeaderActionsSingleExpenseTableViewTest.tsx deleted file mode 100644 index d9209c32a9d1..000000000000 --- a/tests/ui/components/MoneyReportHeaderActionsSingleExpenseTableViewTest.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import {render} from '@testing-library/react-native'; - -import MoneyReportHeaderActions from '@components/MoneyReportHeaderActions'; -import MoneyReportHeaderSecondaryActions from '@components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions'; -import MoneyReportHeaderPrimaryAction from '@components/MoneyReportHeaderPrimaryAction'; - -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; - -import CONST from '@src/CONST'; - -import React from 'react'; - -const REPORT_ID = '1001'; - -jest.mock('@hooks/useOnyx', () => jest.fn(() => [undefined, {status: 'loaded'}])); -jest.mock('@hooks/useExportAgainModal', () => jest.fn(() => ({triggerExportOrConfirm: jest.fn()}))); -jest.mock('@hooks/useLayoutSpacing', () => jest.fn(() => ({pageGutter: {}}))); -jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({shouldUseNarrowLayout: false, isMediumScreenWidth: false, isInLandscapeMode: false}))); -jest.mock('@hooks/useResponsiveLayoutOnWideRHP', () => jest.fn(() => ({isWideRHPDisplayedOnWideLayout: false, isSuperWideRHPDisplayedOnWideLayout: false, shouldUseNarrowLayout: false}))); -jest.mock('@hooks/useSingleExpenseReportView', () => jest.fn()); - -// useThemeStyles throws without a ; return a proxy that yields an empty style object for any key. -jest.mock('@hooks/useThemeStyles', () => { - const styleProxy = new Proxy({}, {get: () => ({})}); - return jest.fn(() => styleProxy); -}); - -// A single-expense report always has a transaction thread, whichever view it is shown in. -jest.mock('@components/MoneyReportTransactionThreadContext', () => ({ - __esModule: true, - useMoneyReportTransactionThread: jest.fn(() => ({transactionThreadReportID: '1002'})), -})); - -// The user has selected the report's only expense. -jest.mock('@components/Search/SearchContext', () => ({ - __esModule: true, - useSearchSelectionContext: jest.fn(() => ({selectedTransactionIDs: ['tx-1']})), - useSearchSelectionActions: jest.fn(() => ({clearSelectedTransactions: jest.fn()})), -})); - -jest.mock('@components/MoneyReportHeaderPrimaryAction', () => jest.fn(() => null)); -jest.mock('@components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions', () => jest.fn(() => null)); -jest.mock('@components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown', () => jest.fn(() => null)); - -const mockedUseSingleExpenseReportView = jest.mocked(useSingleExpenseReportView); -const mockedPrimaryAction = jest.mocked(MoneyReportHeaderPrimaryAction); -const mockedSecondaryActions = jest.mocked(MoneyReportHeaderSecondaryActions); - -function mockSingleExpenseReportView(shouldUseTableViewForSingleExpense: boolean) { - mockedUseSingleExpenseReportView.mockReturnValue({ - singleExpenseReportView: shouldUseTableViewForSingleExpense ? CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE : CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE, - shouldUseTableViewForSingleExpense, - isLoading: false, - }); -} - -function renderActions() { - return render( - , - ); -} - -describe('MoneyReportHeaderActions on a single-expense report', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('disables Submit and More while the expense is selected in the table view', () => { - // Given a single-expense report shown in the table view, where its expense is a selectable row - mockSingleExpenseReportView(true); - - // When the header renders while that expense is selected - renderActions(); - - // Then the report-level actions are disabled so they can't be mixed up with the bulk actions on the selection - expect(mockedPrimaryAction.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({isDisabled: true})); - expect(mockedSecondaryActions.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({isDisabled: true})); - }); - - it('keeps Submit and More enabled in the expense view', () => { - // Given a single-expense report shown in the expense view, which has no selectable rows - mockSingleExpenseReportView(false); - - // When the header renders with a leftover selection - renderActions(); - - // Then the report-level actions stay enabled, as there is no bulk selection the user can act on - expect(mockedPrimaryAction.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({isDisabled: false})); - expect(mockedSecondaryActions.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({isDisabled: false})); - }); -}); diff --git a/tests/ui/components/MoneyReportHeaderSelectionModeTest.tsx b/tests/ui/components/MoneyReportHeaderSelectionModeTest.tsx index 020dca0327d0..323e51a4fca4 100644 --- a/tests/ui/components/MoneyReportHeaderSelectionModeTest.tsx +++ b/tests/ui/components/MoneyReportHeaderSelectionModeTest.tsx @@ -350,42 +350,5 @@ describe('MoneyReportHeader selection mode header', () => { expect(mockTurnOffMobileSelectionMode).toHaveBeenCalled(); expect(lastHeaderTitle()).toBeUndefined(); }); - - describe('when the user chose the table view for single-expense reports', () => { - beforeEach(() => { - mockUseOnyx.mockImplementation((key) => { - if (key === `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`) { - return createOnyxResult(report); - } - if (key === ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW) { - return createOnyxResult(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - } - return createOnyxResult(undefined); - }); - }); - - it('keeps selection mode on in a narrow window when a single transaction is left', () => { - // Given a single-expense report that shows its expense in the table, where a row can be selected - mockTransactions([null]); - - // When the header renders with selection mode on - renderHeader(); - - // Then selection mode stays on, so the user can select the only row - expect(mockTurnOffMobileSelectionMode).not.toHaveBeenCalled(); - expect(lastHeaderTitle()).toBe('common.selectMultiple'); - }); - - it('turns selection mode off in a narrow window when no transactions are left', () => { - // Given the report's only expense was deleted, so there is no row left to select - mockTransactions([CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE]); - - // When the header renders with selection mode on - renderHeader(); - - // Then selection mode is turned off - expect(mockTurnOffMobileSelectionMode).toHaveBeenCalled(); - }); - }); }); }); diff --git a/tests/ui/components/SelectionToolbarSubmitToPopoverTest.tsx b/tests/ui/components/SelectionToolbarSubmitToPopoverTest.tsx deleted file mode 100644 index ca63091664d0..000000000000 --- a/tests/ui/components/SelectionToolbarSubmitToPopoverTest.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import {render} from '@testing-library/react-native'; - -import SelectionToolbar from '@components/MoneyRequestReportView/SelectionToolbar'; -import {useOpenReportSubmitToPopover} from '@components/ReportSubmitToPopoverAnchor'; - -import useReportSubmitToPopover from '@hooks/useReportSubmitToPopover'; -import useSelectionModeReportActions from '@hooks/useSelectionModeReportActions'; - -import type * as ReactNavigationNative from '@react-navigation/native'; - -import React from 'react'; - -const REPORT_ID = '1001'; - -jest.mock('@hooks/useOnyx', () => jest.fn(() => [undefined, {status: 'loaded'}])); -jest.mock('@hooks/useLocalize', () => jest.fn(() => ({translate: (key: string) => key}))); -jest.mock('@hooks/useNetworkWithOfflineStatus', () => jest.fn(() => ({isOffline: false}))); -jest.mock('@hooks/useResponsiveLayoutOnWideRHP', () => jest.fn(() => ({shouldUseNarrowLayout: false, isInLandscapeMode: false}))); -jest.mock('@hooks/useMobileSelectionMode', () => jest.fn(() => false)); -jest.mock('@hooks/useConfirmModal', () => jest.fn(() => ({showConfirmModal: jest.fn()}))); -jest.mock('@hooks/useFilterSelectedTransactions', () => jest.fn()); -jest.mock('@hooks/useSelectedTransactionsActions', () => - jest.fn(() => ({options: [], isDuplicateOptionVisible: false, setDuplicateHandler: jest.fn(), allTransactions: undefined, allReports: undefined})), -); -jest.mock('@hooks/useSelectionModeReportActions', () => jest.fn()); -jest.mock('@hooks/useReportSubmitToPopover', () => jest.fn()); -jest.mock('@components/MoneyRequestReportView/useShouldShowReportBulkActionBar', () => jest.fn(() => false)); -jest.mock('@components/DelegateNoAccessModalProvider', () => ({ - __esModule: true, - useDelegateNoAccessState: jest.fn(() => ({isDelegateAccessRestricted: false})), - useDelegateNoAccessActions: jest.fn(() => ({showDelegateNoAccessModal: jest.fn()})), -})); -jest.mock('@components/DecisionModal', () => jest.fn(() => null)); - -// useThemeStyles throws without a ; return a proxy that yields an empty style object for any key. -jest.mock('@hooks/useThemeStyles', () => { - const styleProxy = new Proxy({}, {get: () => ({})}); - return jest.fn(() => styleProxy); -}); - -// The user has selected the report's only expense, so the toolbar is shown. -jest.mock('@components/Search/SearchContext', () => ({ - __esModule: true, - useSearchSelectionContext: jest.fn(() => ({selectedTransactionIDs: ['tx-1'], currentSelectedTransactionReportID: '1001'})), - useSearchSelectionActions: jest.fn(() => ({clearSelectedTransactions: jest.fn(), setSelectedTransactions: jest.fn(), setCurrentSelectedTransactionReportID: jest.fn()})), -})); - -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - __esModule: true, - useRoute: jest.fn(() => ({key: 'route-1', name: 'Report', params: {}})), - useFocusEffect: jest.fn(), -})); - -const mockedUseSelectionModeReportActions = jest.mocked(useSelectionModeReportActions); -const mockedUseReportSubmitToPopover = jest.mocked(useReportSubmitToPopover); - -describe('SelectionToolbar bulk Submit', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('opens the submit-to popover from the selection bulk actions', () => { - // Given the submit-to popover's opener, which a Submit workspace's bulk Submit calls instead of submitting directly - const openReportSubmitToPopover = jest.fn(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- only the opener matters here, and the popover element is never rendered - mockedUseReportSubmitToPopover.mockReturnValue({ - anchorRef: {current: null}, - openReportSubmitToPopover, - closeReportSubmitToPopover: jest.fn(), - reportSubmitToPopover: null, - isReportSubmitToPopoverVisible: false, - isReportSubmitToDismissGuardActive: false, - consumeIgnoreNextSearchSubmitPress: () => false, - } as unknown as ReturnType); - - // And the toolbar's bulk report actions, which get that opener from context - let openFromBulkSubmit: ReturnType | undefined; - mockedUseSelectionModeReportActions.mockImplementation(() => { - openFromBulkSubmit = useOpenReportSubmitToPopover(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- the toolbar only reads these fields on the path this test renders - return {selectionModeReportLevelActions: [], allExpensesSelected: true} as unknown as ReturnType; - }); - - render( - , - ); - - // When bulk Submit asks to open the submit-to popover - openFromBulkSubmit?.(); - - // Then the popover opens, rather than the click falling through to the no-op default with no provider above the toolbar - expect(openReportSubmitToPopover).toHaveBeenCalledTimes(1); - }); -}); diff --git a/tests/unit/MoneyRequestReportUtilsTest.ts b/tests/unit/MoneyRequestReportUtilsTest.ts index ada2aae0fdf7..3da3991823ea 100644 --- a/tests/unit/MoneyRequestReportUtilsTest.ts +++ b/tests/unit/MoneyRequestReportUtilsTest.ts @@ -1,13 +1,10 @@ import type {TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; import { - getEffectiveTransactionThreadReportID, getReportIDForTransaction, isBillableEnabledOnPolicy, isEveryReportTransactionSelected, isSelectableReportTransaction, - shouldDisplayReportTableView, - shouldUseMultiExpenseReportLayout, shouldWaitForTransactions, } from '@libs/MoneyRequestReportUtils'; @@ -278,67 +275,4 @@ describe('MoneyRequestReportUtils', () => { expect(shouldWaitForTransactions(reportBaseMock, [], reportLoadingState, false, false)).toBe(true); }); }); - - describe('shouldDisplayReportTableView', () => { - const singleTransaction = createMock({transactionID: '555', reportID: reportBaseMock.reportID}); - const secondTransaction = createMock({transactionID: '556', reportID: reportBaseMock.reportID}); - - test('uses the single-expense view for a single-expense report by default', () => { - // Given a report with one expense and no saved preference - // Then it renders in the single-expense view - expect(shouldDisplayReportTableView(reportBaseMock, [singleTransaction])).toBe(false); - }); - - test('uses the table view for a single-expense report when the user picked the table view', () => { - // Given a report with one expense and a user who picked the table view - // Then it renders in the table view - expect(shouldDisplayReportTableView(reportBaseMock, [singleTransaction], true)).toBe(true); - }); - - test('always uses the table view for a report with more than one expense', () => { - // Given a report with two expenses, the preference doesn't apply - expect(shouldDisplayReportTableView(reportBaseMock, [singleTransaction, secondTransaction])).toBe(true); - expect(shouldDisplayReportTableView(reportBaseMock, [singleTransaction, secondTransaction], true)).toBe(true); - }); - }); - - describe('shouldUseMultiExpenseReportLayout', () => { - test('uses the multi-expense layout for a report with more than one expense', () => { - // Given a report with two expenses, the preference doesn't apply - expect(shouldUseMultiExpenseReportLayout(2, false)).toBe(true); - expect(shouldUseMultiExpenseReportLayout(2, true)).toBe(true); - }); - - test('uses the multi-expense layout for a single-expense report only when the user picked the table view', () => { - // Given a report with one expense - // Then only the table view preference gives it the multi-expense layout - expect(shouldUseMultiExpenseReportLayout(1, false)).toBe(false); - expect(shouldUseMultiExpenseReportLayout(1, true)).toBe(true); - }); - - test('keeps the default layout for an empty report', () => { - // Given a report with no expenses, the table view preference doesn't apply - expect(shouldUseMultiExpenseReportLayout(0, true)).toBe(false); - }); - }); - - describe('getEffectiveTransactionThreadReportID', () => { - test('sends comments to the transaction thread in the single-expense view', () => { - // Given a single-expense report in the single-expense view - // Then comments go to the transaction thread - expect(getEffectiveTransactionThreadReportID('123', false, false)).toBe('123'); - }); - - test('sends comments to the report itself in the table view', () => { - // Given a single-expense report in the table view, where the thread's actions aren't shown - // Then comments go to the report itself - expect(getEffectiveTransactionThreadReportID('123', false, true)).toBeUndefined(); - }); - - test('sends comments to the report itself for a sent money report', () => { - // Given a sent money report - // Then comments go to the report itself - expect(getEffectiveTransactionThreadReportID('123', true, false)).toBeUndefined(); - }); - }); }); diff --git a/tests/unit/ReportActionsWithInboxTabDeferredMountTest.tsx b/tests/unit/ReportActionsWithInboxTabDeferredMountTest.tsx index a2d15bdefe73..557ded97b566 100644 --- a/tests/unit/ReportActionsWithInboxTabDeferredMountTest.tsx +++ b/tests/unit/ReportActionsWithInboxTabDeferredMountTest.tsx @@ -26,7 +26,7 @@ jest.mock( jest.mock('@hooks/useMarkOpenReportEndOnSkeleton', () => () => undefined); jest.mock('@hooks/useNetwork', () => () => ({isOffline: false})); -jest.mock('@hooks/useOnyx', () => () => [undefined, {status: 'loaded'}]); +jest.mock('@hooks/useOnyx', () => () => [undefined]); jest.mock('@hooks/usePaginatedReportActions', () => () => ({reportActions: {}})); jest.mock('@hooks/useReportTransactionsCollection', () => () => ({})); diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index 6d1e8b4e6c86..b0cb3e759876 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -176,118 +176,6 @@ describe('getSecondaryAction', () => { ).toEqual(result); }); - it('includes TOGGLE_SINGLE_EXPENSE_VIEW right before VIEW_DETAILS on a report with one expense', () => { - // Given a report that holds exactly one expense - const report = createMock({reportID: REPORT_ID}); - const policy = createMock({}); - const transaction = createMock({transactionID: '1', reportID: REPORT_ID}); - - // When the More menu actions are computed - const result = getSecondaryReportActions({ - currentUserLogin: EMPLOYEE_EMAIL, - currentUserAccountID: EMPLOYEE_ACCOUNT_ID, - submitterLogin: '', - report, - chatReport, - reportTransactions: [transaction], - originalTransaction: createMock({}), - violations: {}, - bankAccountList: {}, - policy, - rules: undefined, - cardList: undefined, - }); - - // Then the option to switch views is offered, just before "View details" - const toggleIndex = result.indexOf(CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW); - expect(toggleIndex).toBeGreaterThan(-1); - expect(result.at(toggleIndex + 1)).toBe(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS); - }); - - it('does not include TOGGLE_SINGLE_EXPENSE_VIEW on a report with more than one expense', () => { - // Given a report with two expenses, which always uses the table view - const report = createMock({reportID: REPORT_ID}); - const policy = createMock({}); - const firstTransaction = createMock({transactionID: '1', reportID: REPORT_ID}); - const secondTransaction = createMock({transactionID: '2', reportID: REPORT_ID}); - - // When the More menu actions are computed - const result = getSecondaryReportActions({ - currentUserLogin: EMPLOYEE_EMAIL, - currentUserAccountID: EMPLOYEE_ACCOUNT_ID, - submitterLogin: '', - report, - chatReport, - reportTransactions: [firstTransaction, secondTransaction], - originalTransaction: createMock({}), - violations: {}, - bankAccountList: {}, - policy, - rules: undefined, - cardList: undefined, - }); - - // Then there's no option to switch views - expect(result).not.toContain(CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW); - }); - - it('does not include TOGGLE_SINGLE_EXPENSE_VIEW on an empty report', () => { - // Given a report with no expenses - const report = createMock({reportID: REPORT_ID}); - const policy = createMock({}); - - // When the More menu actions are computed - const result = getSecondaryReportActions({ - currentUserLogin: EMPLOYEE_EMAIL, - currentUserAccountID: EMPLOYEE_ACCOUNT_ID, - submitterLogin: '', - report, - chatReport, - reportTransactions: [], - originalTransaction: createMock({}), - violations: {}, - bankAccountList: {}, - policy, - rules: undefined, - cardList: undefined, - }); - - // Then there's no option to switch views - expect(result).not.toContain(CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW); - }); - - it('does not include TOGGLE_SINGLE_EXPENSE_VIEW on a "Pay someone" report', () => { - // Given a "Pay someone" report, whose one expense comes from a pay action with IOUDetails - const report = createMock({reportID: REPORT_ID}); - const policy = createMock({}); - const transaction = createMock({transactionID: '1', reportID: REPORT_ID}); - const sentMoneyAction = createMock({ - reportActionID: '1', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.PAY, IOUTransactionID: '1', IOUDetails: {amount: 100, comment: '', currency: CONST.CURRENCY.USD}}, - }); - - // When the More menu actions are computed - const result = getSecondaryReportActions({ - currentUserLogin: EMPLOYEE_EMAIL, - currentUserAccountID: EMPLOYEE_ACCOUNT_ID, - submitterLogin: '', - report, - chatReport, - reportTransactions: [transaction], - originalTransaction: createMock({}), - violations: {}, - bankAccountList: {}, - policy, - reportActions: [sentMoneyAction], - rules: undefined, - cardList: undefined, - }); - - // Then there's no option to switch views, because the report has no expense thread for a table row to open - expect(result).not.toContain(CONST.REPORT.SECONDARY_ACTIONS.TOGGLE_SINGLE_EXPENSE_VIEW); - }); - it('includes DOWNLOAD_RECEIPTS when at least one transaction has a receipt', () => { const report = createMock({}); const policy = createMock({}); diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index fc2feb608dd8..a549560380ac 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -5460,47 +5460,6 @@ describe('TransactionUtils', () => { }); }); - describe('doesReportKeepExpensesAfterMerge', () => { - const DISCARDED_TRANSACTION_ID = '1'; - const OTHER_TRANSACTION_ID = '2'; - - it('returns true when another expense stays on the report', () => { - // Given a report with the discarded expense and another active expense - const reportTransactions = [ - {...createRandomTransaction(1), transactionID: DISCARDED_TRANSACTION_ID}, - {...createRandomTransaction(2), transactionID: OTHER_TRANSACTION_ID}, - ]; - - // When the merge discards one of them, Then the report still has the other one - expect(TransactionUtils.doesReportKeepExpensesAfterMerge(reportTransactions, new Set([DISCARDED_TRANSACTION_ID]))).toBe(true); - }); - - it('returns false when the merge discards the only expense on the report', () => { - // Given a report whose only expense is the one being discarded - const reportTransactions = [{...createRandomTransaction(1), transactionID: DISCARDED_TRANSACTION_ID}]; - - // When the merge discards it, Then the report has no expenses left, so going back to it would show "Not here" - expect(TransactionUtils.doesReportKeepExpensesAfterMerge(reportTransactions, new Set([DISCARDED_TRANSACTION_ID]))).toBe(false); - }); - - it('returns false when the only other expense is already pending deletion', () => { - // Given a report with the discarded expense and another expense that is already being deleted - const reportTransactions = [ - {...createRandomTransaction(1), transactionID: DISCARDED_TRANSACTION_ID}, - {...createRandomTransaction(2), transactionID: OTHER_TRANSACTION_ID, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, - ]; - - // When the merge discards the first one, Then the pending delete doesn't count, so the report has no expenses left - expect(TransactionUtils.doesReportKeepExpensesAfterMerge(reportTransactions, new Set([DISCARDED_TRANSACTION_ID]))).toBe(false); - }); - - it('returns true when the report transactions are not loaded', () => { - // Given no transactions loaded for the report, so we can't tell whether it would be emptied - // When the merge runs, Then the report is treated as keeping expenses, so the app goes back to it as before - expect(TransactionUtils.doesReportKeepExpensesAfterMerge([], new Set([DISCARDED_TRANSACTION_ID]))).toBe(true); - }); - }); - describe('buildNewTransactionAfterReviewingDuplicates', () => { it('preserves the kept transaction tax amount when the selected tax code matches the existing tax code', () => { const duplicatedTransaction = generateTransaction({ diff --git a/tests/unit/hooks/useExpenseActionsSingleExpenseTableViewDeleteTest.tsx b/tests/unit/hooks/useExpenseActionsSingleExpenseTableViewDeleteTest.tsx deleted file mode 100644 index 989659defe28..000000000000 --- a/tests/unit/hooks/useExpenseActionsSingleExpenseTableViewDeleteTest.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import {act, renderHook, waitFor} from '@testing-library/react-native'; - -import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import {useMoneyReportTransactionThread} from '@components/MoneyReportTransactionThreadContext'; -import OnyxListItemProvider from '@components/OnyxListItemProvider'; - -import useExpenseActions from '@hooks/useExpenseActions'; - -import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; -import showConfirmModalAfterMoreMenuDismiss from '@libs/showConfirmModalAfterMoreMenuDismiss'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; - -import type {ValueOf} from 'type-fest'; - -import React from 'react'; -import Onyx from 'react-native-onyx'; - -import createRandomReportAction from '../../utils/collections/reportActions'; -import {createRandomReport} from '../../utils/collections/reports'; -import createRandomTransaction from '../../utils/collections/transaction'; -import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; - -// The confirm modal tells which delete path ran, so cancel it to stop before any navigation or deletion happens. -jest.mock('@libs/showConfirmModalAfterMoreMenuDismiss', () => ({__esModule: true, default: jest.fn(() => Promise.resolve({action: 'CLOSE'}))})); - -// useConfirmModal reaches into the global ModalContext which isn't mounted here; stub it (the confirm result is -// provided by the showConfirmModalAfterMoreMenuDismiss mock above). -jest.mock('@hooks/useConfirmModal', () => ({__esModule: true, default: () => ({showConfirmModal: jest.fn(), closeModal: jest.fn()})})); - -const mockDeleteTransactions = jest.fn(() => ({action: 'deleted', deletedTransactionThreadReportIDs: []})); -const mockShouldOpenSplitExpenseEditFlowOnDelete = jest.fn(() => false); - -jest.mock('@hooks/useDeleteTransactions', () => ({ - __esModule: true, - default: () => ({deleteTransactions: mockDeleteTransactions, shouldOpenSplitExpenseEditFlowOnDelete: mockShouldOpenSplitExpenseEditFlowOnDelete}), -})); - -// The transaction-thread data normally comes from a context provider fed by Onyx; supply it directly. -jest.mock('@components/MoneyReportTransactionThreadContext', () => ({__esModule: true, useMoneyReportTransactionThread: jest.fn()})); - -const mockShowConfirmModalAfterMoreMenuDismiss = jest.mocked(showConfirmModalAfterMoreMenuDismiss); - -const expenseReportID = '41'; -const transactionID = '42'; -const iouActionID = '43'; - -const wrapper = ({children}: {children: React.ReactNode}) => ( - - {children} - -); - -/** Set up an open report owned by the current user (accountID 0) that holds exactly one of their expenses. */ -async function setUpSingleExpenseReport( - singleExpenseReportView: ValueOf, - reportType: typeof CONST.REPORT.TYPE.EXPENSE | typeof CONST.REPORT.TYPE.IOU = CONST.REPORT.TYPE.EXPENSE, -) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const iouAction = { - ...createRandomReportAction(Number(iouActionID)), - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - actorAccountID: 0, - originalMessage: { - IOUReportID: expenseReportID, - IOUTransactionID: transactionID, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount: 100, - currency: CONST.CURRENCY.USD, - }, - pendingAction: null, - } as ReportAction; - const report: Report = { - ...createRandomReport(Number(expenseReportID), undefined), - type: reportType, - ownerAccountID: 0, - stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS_NUM.OPEN, - }; - const transaction = { - ...createRandomTransaction(Number(transactionID)), - transactionID, - reportID: expenseReportID, - status: CONST.TRANSACTION.STATUS.POSTED, - comment: {originalTransactionID: '44'}, - }; - - jest.mocked(useMoneyReportTransactionThread).mockReturnValue({ - iouTransactionID: transactionID, - requestParentReportAction: iouAction, - transactionThreadReportID: undefined, - transactionThreadReport: undefined, - reportActions: [iouAction], - }); - - await act(async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReportID}`, {[iouActionID]: iouAction}); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, singleExpenseReportView); - }); - await waitForBatchedUpdatesWithAct(); -} - -describe('useExpenseActions - deleting a single-expense report from the More menu', () => { - beforeAll(() => { - Onyx.init({keys: ONYXKEYS, evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS]}); - initOnyxDerivedValues(); - return waitForBatchedUpdatesWithAct(); - }); - - afterEach(async () => { - jest.clearAllMocks(); - mockShouldOpenSplitExpenseEditFlowOnDelete.mockReturnValue(false); - await act(async () => { - await Onyx.clear(); - }); - }); - - it('deletes the report, not the expense, in the table view', async () => { - // Given the user chose the table view for single-expense reports and owns the report's only expense - await setUpSingleExpenseReport(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - const {result} = renderHook(() => useExpenseActions({reportID: expenseReportID, isReportInSearch: false, backTo: undefined}), {wrapper}); - await waitFor(() => { - // Wait for the translated label so the confirm modal below is built with loaded translations - expect(result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.text).toBe('Delete'); - }); - - // When the user selects More > Delete - await act(async () => { - await result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.onSelected?.(); - }); - - // Then the report-level delete runs, which moves the expense to the self DM instead of deleting it - expect(mockShowConfirmModalAfterMoreMenuDismiss).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({title: 'Delete report'})); - expect(mockDeleteTransactions).not.toHaveBeenCalled(); - }); - - it('deletes the expense in the expense view', async () => { - // Given the user kept the default expense view and owns the report's only expense - await setUpSingleExpenseReport(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE); - const {result} = renderHook(() => useExpenseActions({reportID: expenseReportID, isReportInSearch: false, backTo: undefined}), {wrapper}); - await waitFor(() => { - // Wait for the translated label so the confirm modal below is built with loaded translations - expect(result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.text).toBe('Delete'); - }); - - // When the user selects More > Delete - await act(async () => { - await result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.onSelected?.(); - }); - - // Then the expense-level delete runs, as the expense view shows a single expense - expect(mockShowConfirmModalAfterMoreMenuDismiss).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({title: 'Delete expense'})); - }); - - it('deletes the expense of an IOU report in the table view', async () => { - // Given the user chose the table view and owns the only expense of an IOU report, which can't be deleted as a report - await setUpSingleExpenseReport(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE, CONST.REPORT.TYPE.IOU); - const {result} = renderHook(() => useExpenseActions({reportID: expenseReportID, isReportInSearch: false, backTo: undefined}), {wrapper}); - await waitFor(() => { - // Wait for the translated label so the confirm modal below is built with loaded translations - expect(result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.text).toBe('Delete'); - }); - - // When the user selects More > Delete - await act(async () => { - await result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.onSelected?.(); - }); - - // Then the expense-level delete still runs - expect(mockShowConfirmModalAfterMoreMenuDismiss).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({title: 'Delete expense'})); - }); - - it('labels the action as Delete instead of Edit splits in the table view', async () => { - // Given the report's only expense is part of a split that would open the edit splits flow on delete - mockShouldOpenSplitExpenseEditFlowOnDelete.mockReturnValue(true); - await setUpSingleExpenseReport(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - - // When the actions are built in the table view - const {result} = renderHook(() => useExpenseActions({reportID: expenseReportID, isReportInSearch: false, backTo: undefined}), {wrapper}); - - // Then the menu says Delete, matching the report-level delete it runs - await waitFor(() => { - expect(result.current.actions[CONST.REPORT.SECONDARY_ACTIONS.DELETE]?.text).toBe('Delete'); - }); - }); -}); diff --git a/tests/unit/hooks/useSingleExpenseReportView.test.ts b/tests/unit/hooks/useSingleExpenseReportView.test.ts deleted file mode 100644 index 5f566db35088..000000000000 --- a/tests/unit/hooks/useSingleExpenseReportView.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {renderHook, waitFor} from '@testing-library/react-native'; - -import useSingleExpenseReportView from '@hooks/useSingleExpenseReportView'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportAction} from '@src/types/onyx'; - -import Onyx from 'react-native-onyx'; - -import createMock from '../../utils/createMock'; -import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; - -const REPORT_ID = '1'; -const REPORT_ACTION_ID = '10'; - -function buildIOUAction(type: typeof CONST.IOU.REPORT_ACTION_TYPE.CREATE | typeof CONST.IOU.REPORT_ACTION_TYPE.PAY): ReportAction { - return createMock({ - reportActionID: REPORT_ACTION_ID, - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - originalMessage: { - type, - IOUTransactionID: '100', - ...(type === CONST.IOU.REPORT_ACTION_TYPE.PAY && {IOUDetails: {amount: 100, comment: '', currency: CONST.CURRENCY.USD}}), - }, - }); -} - -describe('useSingleExpenseReportView', () => { - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - }); - - beforeEach(async () => { - await Onyx.clear(); - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - await waitForBatchedUpdates(); - }); - - it('uses the table view for a single-expense report when the user picked it', async () => { - // Given the user picked the table view and the report's expense comes from a regular expense action - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION_ID]: buildIOUAction(CONST.IOU.REPORT_ACTION_TYPE.CREATE)}); - await waitForBatchedUpdates(); - - // When the view is read for that report - const {result} = renderHook(() => useSingleExpenseReportView(REPORT_ID)); - - // Then the report uses the table view - await waitFor(() => expect(result.current.shouldUseTableViewForSingleExpense).toBe(true)); - }); - - it('uses the expense view for a "Pay someone" report even when the user picked the table view', async () => { - // Given the user picked the table view and the report is a "Pay someone" report - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION_ID]: buildIOUAction(CONST.IOU.REPORT_ACTION_TYPE.PAY)}); - await waitForBatchedUpdates(); - - // When the view is read for that report - const {result} = renderHook(() => useSingleExpenseReportView(REPORT_ID)); - - // Then the report keeps the expense view, because it has no expense thread for a table row to open, - // while the saved preference itself is unchanged - await waitFor(() => expect(result.current.singleExpenseReportView).toBe(CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE)); - expect(result.current.shouldUseTableViewForSingleExpense).toBe(false); - }); -}); diff --git a/tests/unit/hooks/useTransactionThreadReportID.test.ts b/tests/unit/hooks/useTransactionThreadReportID.test.ts index 1cbd4b1976c8..9c82821e1429 100644 --- a/tests/unit/hooks/useTransactionThreadReportID.test.ts +++ b/tests/unit/hooks/useTransactionThreadReportID.test.ts @@ -99,11 +99,8 @@ function asReportOnyxResult(report: Report | undefined): UseOnyxResult { return [report, loadedReportMetadata]; } -function wireReportOnyx(moneyReport: Report | undefined, chatReport: Report | undefined, hookReportID: string | undefined, singleExpenseReportView?: string): void { +function wireReportOnyx(moneyReport: Report | undefined, chatReport: Report | undefined, hookReportID: string | undefined): void { mockUseOnyx.mockImplementation((key: OnyxKey) => { - if (key === ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW) { - return [singleExpenseReportView, loadedReportMetadata]; - } const moneyReportKey = collectionReportKey(hookReportID); if (key === moneyReportKey) { return asReportOnyxResult(moneyReport); @@ -211,38 +208,6 @@ describe('useTransactionThreadReportID', () => { expect(result.current.reportActions?.map((a) => a.reportActionID)).toEqual(['iou-created-1']); }); - it('returns no effective thread when the user views single-expense reports in the table view', () => { - // Given a single-expense report and a user who picked the table view, where the thread's actions aren't shown - const moneyReport = makeExpenseReportWithChat(); - - wireReportOnyx(moneyReport, makeDMChatReport(), MONEY_REPORT_ID, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - const iouCreate = makeIOUCreatedAction({childReportID: THREAD_REPORT_ID}); - mockUsePaginatedReportActions.mockReturnValue({ - reportActions: [iouCreate], - linkedAction: undefined, - oldestUnreadReportAction: undefined, - sortedAllReportActions: [iouCreate], - hasOlderActions: false, - hasNewerActions: false, - report: moneyReport, - }); - mockUseReportTransactionsCollection.mockReturnValue( - transactionsRecordForReport([ - createMock({ - transactionID: 'txn-1', - reportID: MONEY_REPORT_ID, - }), - ]), - ); - - // When the hook resolves the thread - const {result} = renderHook(() => useTransactionThreadReportID(MONEY_REPORT_ID)); - - // Then the thread is still found, but comments target the report so they show up in the table view - expect(result.current.transactionThreadReportID).toBe(THREAD_REPORT_ID); - expect(result.current.effectiveTransactionThreadReportID).toBeUndefined(); - }); - it('uses CONST.FAKE_REPORT_ID when no childReportID is set on the lone IOU request action', () => { const moneyReport = makeExpenseReportWithChat(); diff --git a/tests/unit/pages/DynamicReportDetailsPage/useReportDetailsDeleteModalTest.ts b/tests/unit/pages/DynamicReportDetailsPage/useReportDetailsDeleteModalTest.ts index 257adb3145fe..a1ef65b18149 100644 --- a/tests/unit/pages/DynamicReportDetailsPage/useReportDetailsDeleteModalTest.ts +++ b/tests/unit/pages/DynamicReportDetailsPage/useReportDetailsDeleteModalTest.ts @@ -2,7 +2,6 @@ import {act, renderHook} from '@testing-library/react-native'; import {ModalActions} from '@components/Modal/Global/ModalContext'; -import getCentralPaneReportID from '@libs/Navigation/helpers/getCentralPaneReportID'; import Navigation from '@libs/Navigation/Navigation'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {navigateBackOnDeleteTransaction} from '@libs/ReportUtils'; @@ -18,7 +17,7 @@ import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {Report, ReportAction, Transaction} from '@src/types/onyx'; +import type {ReportAction} from '@src/types/onyx'; import {StackActions} from '@react-navigation/native'; @@ -51,10 +50,6 @@ jest.mock('@libs/Navigation/Navigation', () => ({ dispatch: (action: unknown) => mockDispatch(action) as unknown, }, })); -jest.mock('@libs/Navigation/helpers/getCentralPaneReportID', () => ({ - __esModule: true, - default: jest.fn(), -})); jest.mock('@libs/Navigation/TransitionTracker', () => ({ __esModule: true, default: {runAfterTransitions: jest.fn()}, @@ -113,7 +108,6 @@ describe('useReportDetailsDeleteModal', () => { jest.clearAllMocks(); mockShowConfirmModal.mockResolvedValue({action: ModalActions.CONFIRM}); jest.mocked(Navigation.getTopmostSearchReportRouteParams).mockReturnValue(undefined); - jest.mocked(getCentralPaneReportID).mockReturnValue(undefined); jest.mocked(Navigation.setNavigationActionToMicrotaskQueue).mockImplementation((callback: () => void) => callback()); jest.mocked(TransitionTracker.runAfterTransitions).mockImplementation(({callback}) => { callback?.(); @@ -183,102 +177,6 @@ describe('useReportDetailsDeleteModal', () => { expect(Navigation.dismissModal).not.toHaveBeenCalled(); }); - it('should close the RHP instead of popping back when deleting the only expense of the report in the super wide RHP', async () => { - // Given a single-expense report in the table view, whose expense was opened on top of the report - setRHPRoutes([ - {name: SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, params: {reportID: '10'}}, - {name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: REPORT_ID}}, - {name: SCREENS.RIGHT_MODAL.REPORT_DETAILS}, - ]); - jest.mocked(getNavigationUrlOnMoneyRequestDelete).mockReturnValue(BACK_URL); - const requestData = buildRequestData({iouReport: createMock({reportID: '10'}), iouReportTransactions: [createMock({transactionID: TRANSACTION_ID})]}); - - // When the delete is confirmed - await confirmDelete(requestData, jest.fn()); - - // Then the RHP closes, because deleting the only expense also deletes the report and going back would show "Not here" - expect(mockDispatch).not.toHaveBeenCalled(); - expect(navigateBackOnDeleteTransaction).not.toHaveBeenCalled(); - expect(setDeleteTransactionNavigateBackUrl).toHaveBeenCalledWith(BACK_URL); - expect(Navigation.dismissModal).toHaveBeenCalledTimes(1); - }); - - it('should still pop back to the super wide RHP when the report keeps other expenses', async () => { - // Given a report with two expenses in the super wide RHP, and one of them opened on top of it - setRHPRoutes([ - {name: SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, params: {reportID: '10'}}, - {name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: REPORT_ID}}, - {name: SCREENS.RIGHT_MODAL.REPORT_DETAILS}, - ]); - const requestData = buildRequestData({ - iouReport: createMock({reportID: '10'}), - iouReportTransactions: [createMock({transactionID: TRANSACTION_ID}), createMock({transactionID: '1001'})], - }); - - // When the delete is confirmed - await confirmDelete(requestData, jest.fn()); - - // Then the RHP pops back to the report, which still exists - expect(mockDispatch).toHaveBeenCalledWith({...StackActions.pop(2), target: RHP_KEY}); - expect(Navigation.dismissModal).not.toHaveBeenCalled(); - }); - - it('should close the RHP when the only other expense on the report is already pending deletion', async () => { - // Given a report in the super wide RHP whose second expense was deleted offline, so only the opened expense is still on the report - setRHPRoutes([ - {name: SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, params: {reportID: '10'}}, - {name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: REPORT_ID}}, - {name: SCREENS.RIGHT_MODAL.REPORT_DETAILS}, - ]); - jest.mocked(getNavigationUrlOnMoneyRequestDelete).mockReturnValue(BACK_URL); - const requestData = buildRequestData({ - iouReport: createMock({reportID: '10'}), - iouReportTransactions: [ - createMock({transactionID: TRANSACTION_ID}), - createMock({transactionID: '1001', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}), - ], - }); - - // When the delete is confirmed - await confirmDelete(requestData, jest.fn()); - - // Then the RHP closes, because the pending-delete expense doesn't count and deleting this one also deletes the report - expect(mockDispatch).not.toHaveBeenCalled(); - expect(navigateBackOnDeleteTransaction).not.toHaveBeenCalled(); - expect(Navigation.dismissModal).toHaveBeenCalledTimes(1); - }); - - it('should close the RHP without navigating away when deleting the only expense from a Search report outside the super wide RHP', async () => { - // Given the default expense view, where the expense was opened from Search without a super wide RHP below it - setRHPRoutes([{name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: REPORT_ID}}, {name: SCREENS.RIGHT_MODAL.REPORT_DETAILS}]); - jest.mocked(getNavigationUrlOnMoneyRequestDelete).mockReturnValue(BACK_URL); - const requestData = buildRequestData({iouReport: createMock({reportID: '10'}), iouReportTransactions: [createMock({transactionID: TRANSACTION_ID})]}); - - // When the delete is confirmed - await confirmDelete(requestData, jest.fn()); - - // Then the RHP closes back to Search, because closing it doesn't reveal the deleted report, so the user isn't sent to the chat - expect(Navigation.dismissModal).toHaveBeenCalledTimes(1); - expect(navigateBackOnDeleteTransaction).not.toHaveBeenCalled(); - expect(setDeleteTransactionNavigateBackUrl).not.toHaveBeenCalled(); - }); - - it('should navigate back instead of closing the RHP when deleting the only expense of the report in the Inbox central pane', async () => { - // Given a narrow layout, where the single-expense report in the table view sits in the Inbox central pane and its expense was opened in the RHP - setRHPRoutes([{name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: REPORT_ID}}, {name: SCREENS.RIGHT_MODAL.REPORT_DETAILS}]); - jest.mocked(getCentralPaneReportID).mockReturnValue('10'); - jest.mocked(getNavigationUrlOnMoneyRequestDelete).mockReturnValue(BACK_URL); - const requestData = buildRequestData({iouReport: createMock({reportID: '10'}), iouReportTransactions: [createMock({transactionID: TRANSACTION_ID})]}); - - // When the delete is confirmed - await confirmDelete(requestData, jest.fn()); - - // Then the app navigates back to the chat, because closing the RHP would reveal the deleted report and show "Not here" - expect(Navigation.dismissModal).not.toHaveBeenCalled(); - expect(setDeleteTransactionNavigateBackUrl).toHaveBeenCalledWith(BACK_URL); - expect(navigateBackOnDeleteTransaction).toHaveBeenCalledWith(BACK_URL); - }); - it('should navigate to the track expense URL when the deleted expense is a tracked expense', async () => { // Given a tracked expense whose delete leads back to a known report jest.mocked(getNavigationUrlAfterTrackExpenseDelete).mockReturnValue(BACK_URL); diff --git a/tests/unit/shouldShowPushNotificationTest.ts b/tests/unit/shouldShowPushNotificationTest.ts deleted file mode 100644 index bad9c34a3488..000000000000 --- a/tests/unit/shouldShowPushNotificationTest.ts +++ /dev/null @@ -1,87 +0,0 @@ -import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; - -import {shouldShowReportActionNotification} from '@userActions/Report'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportAction} from '@src/types/onyx'; - -import type {PushPayload} from '@ua/react-native-airship'; - -import Onyx from 'react-native-onyx'; - -import createMock from '../utils/createMock'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - -const REPORT_ID = '10'; -const TRANSACTION_THREAD_REPORT_ID = '20'; - -jest.mock('@libs/Navigation/Navigation', () => ({ - __esModule: true, - default: {getTopmostReportId: jest.fn(() => '10')}, -})); -jest.mock('@userActions/Report', () => ({ - shouldShowReportActionNotification: jest.fn(() => true), -})); - -function buildPushPayload(): PushPayload { - return createMock({ - extras: {payload: {type: 'reportComment', reportID: Number(TRANSACTION_THREAD_REPORT_ID), reportActionID: '1', title: '', subtitle: ''}}, - }); -} - -describe('shouldShowPushNotification', () => { - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - }); - - beforeEach(async () => { - jest.clearAllMocks(); - jest.spyOn(ReportActionsUtils, 'getOneTransactionThreadReportID').mockReturnValue(TRANSACTION_THREAD_REPORT_ID); - await Onyx.clear(); - await waitForBatchedUpdates(); - }); - - it('passes the expense thread of the open report when single-expense reports use the expense view', async () => { - // Given single-expense reports use the expense view, which shows the expense thread's comments - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.EXPENSE); - await waitForBatchedUpdates(); - - // When a push notification arrives for a comment on that expense thread - shouldShowPushNotification(buildPushPayload()); - - // Then the thread counts as part of the open report, so its comment doesn't notify while the report is open - expect(shouldShowReportActionNotification).toHaveBeenCalledWith(TRANSACTION_THREAD_REPORT_ID, TRANSACTION_THREAD_REPORT_ID, expect.any(Number), null, true); - }); - - it('leaves the expense thread out when single-expense reports use the table view', async () => { - // Given single-expense reports use the table view, which doesn't show the expense thread's comments - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - await waitForBatchedUpdates(); - - // When a push notification arrives for a comment on that expense thread - shouldShowPushNotification(buildPushPayload()); - - // Then the thread isn't treated as part of the open report, so its comment can still notify - expect(shouldShowReportActionNotification).toHaveBeenCalledWith(TRANSACTION_THREAD_REPORT_ID, undefined, expect.any(Number), null, true); - }); - - it('keeps the expense thread for a "Pay someone" report in the table view', async () => { - // Given the table view is on, but the open report is a "Pay someone" report, which always uses the expense view - const sentMoneyAction = createMock({ - reportActionID: '100', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.PAY, IOUDetails: {amount: 100, currency: CONST.CURRENCY.USD, comment: ''}, amount: 100, currency: CONST.CURRENCY.USD}, - }); - await Onyx.set(ONYXKEYS.NVP_SINGLE_EXPENSE_REPORT_VIEW, CONST.REPORT_LAYOUT.SINGLE_EXPENSE_REPORT_VIEW.TABLE); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[sentMoneyAction.reportActionID]: sentMoneyAction}); - await waitForBatchedUpdates(); - - // When a push notification arrives for a comment on that expense thread - shouldShowPushNotification(buildPushPayload()); - - // Then the thread still counts as part of the open report, as it does in the expense view - expect(shouldShowReportActionNotification).toHaveBeenCalledWith(TRANSACTION_THREAD_REPORT_ID, TRANSACTION_THREAD_REPORT_ID, expect.any(Number), null, true); - }); -});