diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 12608eac2274..b604667a88e9 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -283,6 +283,7 @@ const CONST = { POPOVER_DATE_RANGE_WIDTH: 672, POPOVER_DATE_MAX_HEIGHT: 366, POPOVER_DATE_MIN_HEIGHT: 322, + POPOVER_REPORT_SUBMIT_TO_CONTENT_HEIGHT: 416, ADVANCED_FILTERS_POPOVER_HEIGHT: 520, ADVANCED_FILTERS_POPOVER_WIDTH: 582, ADVANCED_FILTERS_CONTENT_WIDTH: 331, diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index a40dfdd7d9c8..9355dcf571b0 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -14,6 +14,7 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon import NavigationDeferredMount from '@components/NavigationDeferredMount'; import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; +import {ReportSubmitToPopoverAnchor} from '@components/ReportSubmitToPopoverAnchor'; import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies'; @@ -76,7 +77,7 @@ type MoneyReportHeaderSecondaryActionsProps = { dropdownMenuRef?: React.RefObject; }; -function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isReportInSearch, backTo, dropdownMenuRef}: MoneyReportHeaderSecondaryActionsProps) { +function MoneyReportHeaderSecondaryActionsContent({reportID, primaryAction, isReportInSearch, backTo, dropdownMenuRef}: MoneyReportHeaderSecondaryActionsProps) { const {isPaidAnimationRunning, isApprovedAnimationRunning, startAnimation, startApprovedAnimation, startSubmittingAnimation} = usePaymentAnimationsContext(); const {openHoldMenu, openPDFDownload, openHoldEducational, openRejectModal} = useMoneyReportHeaderModals(); @@ -418,6 +419,14 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo ); } +function MoneyReportHeaderSecondaryActionsInner(props: MoneyReportHeaderSecondaryActionsProps) { + return ( + + + + ); +} + function MoneyReportHeaderSecondaryActionsPlaceholder({primaryAction}: {primaryAction: ValueOf | ''}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx index 903f5a4e6856..aa762446f4ff 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx @@ -3,6 +3,7 @@ import {isTrackIntentUserSelector} from '@selectors/Onboarding'; import React from 'react'; import AnimatedSubmitButton from '@components/AnimatedSubmitButton'; import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; +import {ReportSubmitToPopoverAnchor, useOpenReportSubmitToPopover} from '@components/ReportSubmitToPopoverAnchor'; import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed'; @@ -17,7 +18,7 @@ import useStrictPolicyRules from '@hooks/useStrictPolicyRules'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {search} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; +import {hasDynamicExternalWorkflow, isSubmitPolicy} from '@libs/PolicyUtils'; import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils'; import {hasViolations as hasViolationsReportUtils, shouldBlockSubmitDueToPreventSelfApproval, shouldBlockSubmitDueToStrictPolicyRules, shouldShowMarkAsDone} from '@libs/ReportUtils'; import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasOnlyPendingCardTransactions, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils'; @@ -26,17 +27,37 @@ import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +const ANCHOR_ALIGNMENT = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, +}; + type SubmitPrimaryActionProps = { reportID: string | undefined; }; function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) { + const {startSubmittingAnimation} = usePaymentAnimationsContext(); + + return ( + + + + ); +} + +function SubmitPrimaryActionContent({reportID}: SubmitPrimaryActionProps) { const {isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation} = usePaymentAnimationsContext(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {accountID, email} = useCurrentUserPersonalDetails(); const {isBetaEnabled} = usePermissions(); const {areStrictPolicyRulesEnabled} = useStrictPolicyRules(); + const openReportSubmitToPopover = useOpenReportSubmitToPopover(); const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); @@ -95,6 +116,11 @@ function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) { } confirmPendingRTERAndProceed(() => { + if (isSubmitPolicy(policy) && reportID) { + openReportSubmitToPopover(); + return; + } + submitReport({ expenseReport: moneyRequestReport, policy, diff --git a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx index 84e20254a316..371b02d55726 100644 --- a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx +++ b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx @@ -8,6 +8,7 @@ import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalMod import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu'; +import {ReportSubmitToPopoverAnchor} from '@components/ReportSubmitToPopoverAnchor'; import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler'; import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import useConfirmModal from '@hooks/useConfirmModal'; @@ -250,18 +251,20 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool - + + + + + + ); +} + +function SubmitActionButtonContent({iouReportID, isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation}: SubmitActionButtonProps) { const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const currentUserDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserDetails.accountID; const currentUserEmail = currentUserDetails.email ?? ''; const {isBetaEnabled} = usePermissions(); + const openReportSubmitToPopover = useOpenReportSubmitToPopover(); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`); @@ -45,7 +69,6 @@ function SubmitActionButton({iouReportID, isSubmittingAnimationRunning, stopAnim const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector}); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const {isOffline} = useNetwork(); - const isDEWSubmission = hasDynamicExternalWorkflow(policy); const reportTransactionsCollection = useReportTransactionsCollection(iouReportID); const transactions = Object.values(reportTransactionsCollection ?? {}).filter( (t): t is Transaction => !!t && (isOffline || t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), @@ -54,49 +77,60 @@ function SubmitActionButton({iouReportID, isSubmittingAnimationRunning, stopAnim const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail); const hasAnyPendingRTERViolation = hasAnyPendingRTERViolationTransactionUtils(transactions, transactionViolations, currentUserEmail, currentUserAccountID, iouReport, policy); + const isDEWSubmission = hasDynamicExternalWorkflow(policy); const handleMarkPendingRTERTransactionsAsCash = () => { markPendingRTERTransactionsAsCash(transactions, transactionViolations, Object.values(reportActions ?? {})); }; const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash); + + const handleSubmit = () => { + if (hasOnlyPendingCardTransactions(transactions)) { + showPendingCardTransactionsBlockModal(showConfirmModal, translate); + return; + } + + confirmPendingRTERAndProceed(() => { + if (isSubmitPolicy(policy) && iouReportID) { + openReportSubmitToPopover(); + return; + } + + submitReport({ + expenseReport: iouReport, + policy, + currentUserAccountIDParam: currentUserAccountID, + currentUserEmailParam: currentUserEmail, + hasViolations, + isASAPSubmitBetaEnabled, + expenseReportCurrentNextStepDeprecated: iouReportNextStep, + userBillingGracePeriodEnds, + amountOwed, + onSubmitted: startSubmittingAnimation, + ownerBillingGracePeriodEnd, + delegateEmail, + }); + }); + }; + const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({ isTrackIntentUser, report: iouReport, policy, }); + return ( { - if (hasOnlyPendingCardTransactions(transactions)) { - showPendingCardTransactionsBlockModal(showConfirmModal, translate); - return; - } - confirmPendingRTERAndProceed(() => { - submitReport({ - expenseReport: iouReport, - policy, - currentUserAccountIDParam: currentUserAccountID, - currentUserEmailParam: currentUserEmail, - hasViolations, - isASAPSubmitBetaEnabled, - expenseReportCurrentNextStepDeprecated: iouReportNextStep, - userBillingGracePeriodEnds, - amountOwed, - onSubmitted: startSubmittingAnimation, - ownerBillingGracePeriodEnd, - delegateEmail, - }); - }); - }} + onPress={handleSubmit} isSubmittingAnimationRunning={isSubmittingAnimationRunning} onAnimationFinish={stopAnimation} + sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.SUBMIT_BUTTON} isDEWSubmission={isDEWSubmission} reportID={iouReportID} - sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.SUBMIT_BUTTON} /> ); } diff --git a/src/components/ReportSubmitToPopoverAnchor.tsx b/src/components/ReportSubmitToPopoverAnchor.tsx new file mode 100644 index 000000000000..1eadcd6ab2ac --- /dev/null +++ b/src/components/ReportSubmitToPopoverAnchor.tsx @@ -0,0 +1,289 @@ +import type {RefObject} from 'react'; +import React, {createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import useReportSubmitToPopover from '@hooks/useReportSubmitToPopover'; +import type {ReportSubmitToPopoverOpenOptions} from '@hooks/useReportSubmitToPopover'; +import CONST from '@src/CONST'; +import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; + +/** Positions the submit-to popover below the Search row Submit button (wide layout). */ +const SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT: AnchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, +}; + +type OpenReportSubmitToPopover = (options?: ReportSubmitToPopoverOpenOptions) => void; + +const ReportSubmitToPopoverContext = createContext(() => { + // Default: no provider (tests / edge UI). Opening is a no-op. +}); + +const ReportSubmitToPopoverAnchorRefContext = createContext | null>(null); + +type ReportSubmitToPopoverHostContextValue = { + registerAnchor: (reportID: string | undefined, anchorRef: RefObject) => () => void; + openReportSubmitToPopover: (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions) => void; +}; + +const ReportSubmitToPopoverHostContext = createContext(null); + +type SearchSubmitPopoverGuardContextValue = { + isReportSubmitToPopoverVisible: boolean; + isReportSubmitToDismissGuardActive: boolean; + shouldDisableSearchSubmitPress: boolean; + consumeIgnoreNextSearchSubmitPress: () => boolean; +}; + +const defaultSearchSubmitPopoverGuard: SearchSubmitPopoverGuardContextValue = { + isReportSubmitToPopoverVisible: false, + isReportSubmitToDismissGuardActive: false, + shouldDisableSearchSubmitPress: false, + consumeIgnoreNextSearchSubmitPress: () => false, +}; + +const SearchSubmitPopoverGuardContext = createContext(defaultSearchSubmitPopoverGuard); + +function useSearchSubmitPopoverGuard(): SearchSubmitPopoverGuardContextValue { + return useContext(SearchSubmitPopoverGuardContext); +} + +function SearchSubmitPopoverGuardProvider({guard, children}: {guard: SearchSubmitPopoverGuardContextValue; children: React.ReactNode}) { + return {children}; +} + +function useOpenReportSubmitToPopover(): OpenReportSubmitToPopover { + return useContext(ReportSubmitToPopoverContext); +} + +/** Opens the shared Search submit-to popover for a report (e.g. bulk actions outside a list row). */ +function useOpenSearchReportSubmitToPopover() { + const host = useContext(ReportSubmitToPopoverHostContext); + + return useCallback( + (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions) => { + host?.openReportSubmitToPopover(reportID, options); + }, + [host], + ); +} + +type ReportSubmitToPopoverAnchorProps = { + reportID: string | undefined; + onSubmitSuccess?: () => void; + children: React.ReactNode; + anchorAlignment?: AnchorAlignment; +}; + +type ReportSubmitToPopoverHostProps = { + children: React.ReactNode; + anchorAlignment?: AnchorAlignment; +}; + +/** + * Renders a single submit-to popover for Search. List rows register their button anchors here so we do not mount + * a Modal inside each FlashList cell (iOS only showed the backdrop when the modal lived in a recycled row). + */ +function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPopoverHostProps) { + const anchorRegistryRef = useRef>>(new Map()); + const [activeReportID, setActiveReportID] = useState(); + const pendingOpenRef = useRef<{reportID: string; options?: ReportSubmitToPopoverOpenOptions} | null>(null); + + const registerAnchor = useCallback((reportID: string | undefined, anchorRef: RefObject) => { + if (!reportID) { + return () => {}; + } + + anchorRegistryRef.current.set(reportID, anchorRef); + return () => { + anchorRegistryRef.current.delete(reportID); + }; + }, []); + + const getAnchorRef = useCallback(() => { + if (!activeReportID) { + return null; + } + return anchorRegistryRef.current.get(activeReportID) ?? null; + }, [activeReportID]); + + const { + reportSubmitToPopover, + openReportSubmitToPopover: openPopoverForActiveReport, + isReportSubmitToPopoverVisible, + isReportSubmitToDismissGuardActive, + consumeIgnoreNextSearchSubmitPress, + } = useReportSubmitToPopover({ + reportID: activeReportID, + anchorAlignment, + getAnchorRef, + }); + + const searchSubmitPopoverGuard = useMemo( + () => ({ + isReportSubmitToPopoverVisible, + isReportSubmitToDismissGuardActive, + shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive, + consumeIgnoreNextSearchSubmitPress, + }), + [isReportSubmitToPopoverVisible, isReportSubmitToDismissGuardActive, consumeIgnoreNextSearchSubmitPress], + ); + + const openReportSubmitToPopoverForHost = useCallback( + (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions) => { + if (!reportID) { + return; + } + + if (activeReportID === reportID) { + openPopoverForActiveReport(options); + return; + } + + pendingOpenRef.current = {reportID, options}; + setActiveReportID(reportID); + }, + [activeReportID, openPopoverForActiveReport], + ); + + useLayoutEffect(() => { + const pending = pendingOpenRef.current; + if (!activeReportID || !pending || pending.reportID !== activeReportID) { + return; + } + + pendingOpenRef.current = null; + openPopoverForActiveReport(pending.options); + }, [activeReportID, openPopoverForActiveReport]); + + const hostContextValue = useMemo( + () => ({ + registerAnchor, + openReportSubmitToPopover: openReportSubmitToPopoverForHost, + }), + [registerAnchor, openReportSubmitToPopoverForHost], + ); + + return ( + + + {children} + {reportSubmitToPopover} + + + ); +} + +/** Mounts modal + opens callback; descendants use {@link useOpenReportSubmitToPopover} and {@link ReportSubmitToPopoverMeasurableAnchor}. */ +function ReportSubmitToPopoverRoot({reportID, onSubmitSuccess, anchorAlignment, children}: ReportSubmitToPopoverAnchorProps) { + const host = useContext(ReportSubmitToPopoverHostContext); + + if (host) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +function ReportSubmitToPopoverRootWithHost({reportID, host, children}: {reportID: string | undefined; host: ReportSubmitToPopoverHostContextValue; children: React.ReactNode}) { + const anchorRef = useRef(null); + + useEffect(() => host.registerAnchor(reportID, anchorRef), [host, reportID]); + + const openReportSubmitToPopover = useCallback( + (options?: ReportSubmitToPopoverOpenOptions) => { + host.openReportSubmitToPopover(reportID, options); + }, + [host, reportID], + ); + + return ( + + {children} + + ); +} + +function ReportSubmitToPopoverRootWithLocalPopover({reportID, onSubmitSuccess, anchorAlignment, children}: ReportSubmitToPopoverAnchorProps) { + const {anchorRef, openReportSubmitToPopover, reportSubmitToPopover, isReportSubmitToPopoverVisible, isReportSubmitToDismissGuardActive, consumeIgnoreNextSearchSubmitPress} = + useReportSubmitToPopover({ + reportID, + onSubmitSuccess, + anchorAlignment, + }); + + const searchSubmitPopoverGuard = useMemo( + () => ({ + isReportSubmitToPopoverVisible, + isReportSubmitToDismissGuardActive, + shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive, + consumeIgnoreNextSearchSubmitPress, + }), + [isReportSubmitToPopoverVisible, isReportSubmitToDismissGuardActive, consumeIgnoreNextSearchSubmitPress], + ); + + return ( + + + {children} + {reportSubmitToPopover} + + + ); +} + +/** Binds submit-to measurements to children only — use under {@link ReportSubmitToPopoverRoot}. */ +function ReportSubmitToPopoverMeasurableAnchor({children}: {children: React.ReactNode}) { + const anchorRef = useContext(ReportSubmitToPopoverAnchorRefContext); + + if (!anchorRef) { + return children; + } + + return ( + + {children} + + ); +} + +/** Wraps submit controls; exposes {@link useOpenReportSubmitToPopover} to descendants and renders the shared submit-to popover. */ +function ReportSubmitToPopoverAnchor({reportID, onSubmitSuccess, anchorAlignment, children}: ReportSubmitToPopoverAnchorProps) { + return ( + + {children} + + ); +} + +export { + ReportSubmitToPopoverAnchor, + ReportSubmitToPopoverHost, + ReportSubmitToPopoverMeasurableAnchor, + ReportSubmitToPopoverRoot, + SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT, + useOpenReportSubmitToPopover, + useOpenSearchReportSubmitToPopover, + useSearchSubmitPopoverGuard, +}; diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index ada56a885554..d6fa7b78e970 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -9,6 +9,12 @@ import {View} from 'react-native'; import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import Icon from '@components/Icon'; +import { + ReportSubmitToPopoverRoot, + SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT, + useOpenReportSubmitToPopover, + useSearchSubmitPopoverGuard, +} from '@components/ReportSubmitToPopoverAnchor'; import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionContext} from '@components/Search/SearchContext'; import {useRowSelection} from '@components/Search/SearchSelectionProvider'; import BaseListItem from '@components/SelectionList/ListItem/BaseListItem'; @@ -47,7 +53,19 @@ import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow'; /** * An expense report row in search results, showing status badge, total, and participants. */ -function ExpenseReportListItem({ +function ExpenseReportListItem(props: ExpenseReportListItemProps) { + const reportID = 'reportID' in props.item && typeof props.item.reportID === 'string' ? props.item.reportID : undefined; + return ( + + + + ); +} + +function ExpenseReportListItemInner({ item, isLoading, isFocused, @@ -181,6 +199,8 @@ function ExpenseReportListItem({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const {showConfirmModal} = useConfirmModal(); const {showHoldMenu} = useHoldMenuModal(); + const openReportSubmitToPopover = useOpenReportSubmitToPopover(); + const {shouldDisableSearchSubmitPress, consumeIgnoreNextSearchSubmitPress} = useSearchSubmitPopoverGuard(); const {transactions: reportTransactions, violations: reportViolations} = useTransactionsAndViolationsForReport(reportItem.reportID); const liveReportTransactions = useMemo(() => Object.values(reportTransactions), [reportTransactions]); @@ -243,6 +263,9 @@ function ExpenseReportListItem({ }, ownerBillingGracePeriodEnd, amountOwed, + openReportSubmitToPopover, + shouldDisableSearchSubmitPress, + consumeIgnoreNextSearchSubmitPress, onPendingCardTransactionsBlock: () => showPendingCardTransactionsBlockModal(showConfirmModal, translate), currentUserAccountID, currentUserLogin, @@ -276,6 +299,9 @@ function ExpenseReportListItem({ liveReportTransactions, ownerBillingGracePeriodEnd, amountOwed, + openReportSubmitToPopover, + shouldDisableSearchSubmitPress, + consumeIgnoreNextSearchSubmitPress, showConfirmModal, translate, currentUserAccountID, @@ -454,6 +480,7 @@ function ExpenseReportListItem({ isHovered={hovered} isFocused={isFocused} isPendingDelete={isPendingDelete} + shouldDisableActionPointerEvents={shouldDisableSearchSubmitPress} isMarkAsDone={shouldUseMarkAsDoneCopy} /> {getDescription} diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx index 0c311b640d47..5c22f9cda156 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx @@ -2,6 +2,7 @@ import React, {Fragment} from 'react'; import {View} from 'react-native'; import Checkbox from '@components/Checkbox'; import Icon from '@components/Icon'; +import {ReportSubmitToPopoverMeasurableAnchor} from '@components/ReportSubmitToPopoverAnchor'; import DeferredActionCell from '@components/Search/SearchList/ListItem/ActionCell/DeferredActionCell'; import DateCell from '@components/Search/SearchList/ListItem/DateCell'; import ExportedIconCell from '@components/Search/SearchList/ListItem/ExportedIconCell'; @@ -38,6 +39,7 @@ function ExpenseReportListItemRowWide({ isHovered = false, isFocused = false, isPendingDelete = false, + shouldDisableActionPointerEvents = false, isMarkAsDone, }: ExpenseReportListItemRowWideProps) { const StyleUtils = useStyleUtils(); @@ -192,19 +194,21 @@ function ExpenseReportListItemRowWide({ ), [CONST.SEARCH.TABLE_COLUMNS.ACTION]: ( - + + + ), [CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME]: ( diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx index 57c5bdb6bd49..6b1abbca688a 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx @@ -25,6 +25,7 @@ function ExpenseReportListItemRow(props: ExpenseReportListItemRowProps) { isHovered={props.isHovered} isFocused={props.isFocused} isPendingDelete={props.isPendingDelete} + shouldDisableActionPointerEvents={props.shouldDisableActionPointerEvents} columns={props.columns} isMarkAsDone={props.isMarkAsDone} /> diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts index 716e826d7a38..400581cdf9cc 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts @@ -23,6 +23,7 @@ type ExpenseReportListItemRowWideProps = ExpenseReportListItemRowNarrowProps & { isHovered?: boolean; isFocused?: boolean; isPendingDelete?: boolean; + shouldDisableActionPointerEvents?: boolean; columns?: SearchColumnType[]; isMarkAsDone: boolean; }; diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index 5596e7c933bc..2f6007a84805 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -7,6 +7,12 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/ import Icon from '@components/Icon'; import {PressableWithFeedback} from '@components/Pressable'; import ReportSearchHeader from '@components/ReportSearchHeader'; +import { + ReportSubmitToPopoverAnchor, + SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT, + useOpenReportSubmitToPopover, + useSearchSubmitPopoverGuard, +} from '@components/ReportSubmitToPopoverAnchor'; import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import {useRowSelection} from '@components/Search/SearchSelectionProvider'; import type {ListItem} from '@components/SelectionList/types'; @@ -98,6 +104,9 @@ type FirstRowReportHeaderProps = { /** Whether the down arrow is expanded */ isExpanded?: boolean; + /** Whether the action button should be disabled */ + shouldDisableActionPointerEvents?: boolean; + /** Parent chat report resolved from live Onyx with search snapshot fallback */ chatReport?: OnyxEntry; }; @@ -113,6 +122,7 @@ function HeaderFirstRow({ isIndeterminate, onDownArrowClick, isExpanded, + shouldDisableActionPointerEvents = false, chatReport, }: FirstRowReportHeaderProps) { const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'UpArrow']); @@ -195,6 +205,7 @@ function HeaderFirstRow({ hash={reportItem.hash} amount={reportItem.total} extraSmall={!isLargeScreenWidth} + shouldDisablePointerEvents={shouldDisableActionPointerEvents} chatReport={chatReport} /> @@ -203,7 +214,18 @@ function HeaderFirstRow({ ); } -function ReportListItemHeader({ +function ReportListItemHeader(props: ReportListItemHeaderProps) { + return ( + + + + ); +} + +function ReportListItemHeaderInner({ report: reportItem, onSelectRow, onCheckboxPress, @@ -253,6 +275,9 @@ function ReportListItemHeader({ const avatarBorderColor = StyleUtils.getItemBackgroundColorStyle(isSelected, !!isFocused || !!isHovered, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ?? theme.highlightBG; + const openReportSubmitToPopover = useOpenReportSubmitToPopover(); + const {shouldDisableSearchSubmitPress, consumeIgnoreNextSearchSubmitPress} = useSearchSubmitPopoverGuard(); + const handleOnButtonPress = (event?: ModifiedMouseEvent) => { handleActionButtonPress({ hash: currentSearchHash, @@ -269,6 +294,9 @@ function ReportListItemHeader({ personalPolicyID, ownerBillingGracePeriodEnd, amountOwed, + openReportSubmitToPopover, + shouldDisableSearchSubmitPress, + consumeIgnoreNextSearchSubmitPress, onPendingCardTransactionsBlock: () => showPendingCardTransactionsBlockModal(showConfirmModal, translate), currentUserAccountID, currentUserLogin, @@ -297,11 +325,13 @@ function ReportListItemHeader({ onCheckboxPress={onCheckboxPress} isDisabled={isDisabled} canSelectMultiple={canSelectMultiple} + handleOnButtonPress={handleOnButtonPress} avatarBorderColor={avatarBorderColor} isSelectAllChecked={isSelectAllChecked} isIndeterminate={isIndeterminate} onDownArrowClick={onDownArrowClick} isExpanded={isExpanded} + shouldDisableActionPointerEvents={shouldDisableSearchSubmitPress} /> ) : ( @@ -317,6 +347,7 @@ function ReportListItemHeader({ isIndeterminate={isIndeterminate} onDownArrowClick={onDownArrowClick} isExpanded={isExpanded} + shouldDisableActionPointerEvents={shouldDisableSearchSubmitPress} chatReport={chatReport} /> diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx index af18ba0c1268..14c14a836a17 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx @@ -35,6 +35,7 @@ function TransactionListItemNarrow({ isFirstItem, transactionViolations, handleActionButtonPress, + shouldDisableActionPointerEvents, transactionPreviewData, exportedReportActions, nonPersonalAndWorkspaceCards, @@ -118,6 +119,7 @@ function TransactionListItemNarrow({ isActionLoading={isLoading ?? isActionLoading} isSelected={isSelected} isDisabled={!!isDisabled} + shouldDisableActionPointerEvents={shouldDisableActionPointerEvents} dateColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} amountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} taxAmountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx index 24c8412ad478..46a434e6ba8b 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx @@ -35,6 +35,7 @@ function TransactionListItemWide({ isLastItem, transactionViolations, handleActionButtonPress, + shouldDisableActionPointerEvents, transactionPreviewData, exportedReportActions, policyCategories, @@ -178,6 +179,7 @@ function TransactionListItemWide({ isActionLoading={isLoading ?? isActionLoading} isSelected={isSelected} isDisabled={!!isDisabled} + shouldDisableActionPointerEvents={shouldDisableActionPointerEvents} dateColumnSize={dateColumnSize} submittedColumnSize={submittedColumnSize} approvedColumnSize={approvedColumnSize} diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index ba3645a12c6e..3f91b98da310 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -8,6 +8,12 @@ import type {OnyxEntry} from 'react-native-onyx'; // eslint-disable-next-line no-restricted-imports import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import { + ReportSubmitToPopoverAnchor, + SEARCH_REPORT_SUBMIT_TO_POPOVER_ANCHOR_ALIGNMENT, + useOpenReportSubmitToPopover, + useSearchSubmitPopoverGuard, +} from '@components/ReportSubmitToPopoverAnchor'; import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import type {TransactionListItemProps, TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; import useLiveRowCapabilities from '@components/Search/SearchList/ListItem/useLiveRowCapabilities'; @@ -41,7 +47,19 @@ import type {TransactionViolation} from '@src/types/onyx/TransactionViolation'; import TransactionListItemNarrow from './TransactionListItemNarrow'; import TransactionListItemWide from './TransactionListItemWide'; -function TransactionListItem({ +function TransactionListItem(props: TransactionListItemProps) { + const reportID = 'reportID' in props.item && typeof props.item.reportID === 'string' ? props.item.reportID : undefined; + return ( + + + + ); +} + +function TransactionListItemInner({ item, isFocused, showTooltip, @@ -162,6 +180,8 @@ function TransactionListItem({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); + const openReportSubmitToPopover = useOpenReportSubmitToPopover(); + const {shouldDisableSearchSubmitPress, consumeIgnoreNextSearchSubmitPress} = useSearchSubmitPopoverGuard(); const handleActionButtonPress = (event?: Parameters[2]) => { handleActionButtonPressUtil({ @@ -180,6 +200,9 @@ function TransactionListItem({ ownerBillingGracePeriodEnd, amountOwed, onUndelete: () => onUndelete?.(transactionItem), + openReportSubmitToPopover, + shouldDisableSearchSubmitPress, + consumeIgnoreNextSearchSubmitPress, onPendingCardTransactionsBlock: () => showPendingCardTransactionsBlockModal(showConfirmModal, translate), currentUserAccountID, currentUserLogin, @@ -211,6 +234,7 @@ function TransactionListItem({ isActionLoading, transactionViolations, handleActionButtonPress, + shouldDisableActionPointerEvents: shouldDisableSearchSubmitPress, transactionPreviewData, exportedReportActions, policyCategories, diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts index 38423137a79c..27e402307b9b 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts @@ -23,6 +23,7 @@ type TransactionListItemSharedProps = { isLastItem?: boolean; transactionViolations: TransactionViolation[]; handleActionButtonPress: (event?: ModifiedMouseEvent) => void; + shouldDisableActionPointerEvents?: boolean; transactionPreviewData: TransactionPreviewData; exportedReportActions: ReportAction[]; policyCategories?: PolicyCategories; diff --git a/src/components/SelectionList/components/Footer.tsx b/src/components/SelectionList/components/Footer.tsx index 981ba8b5071f..f767c0aafd20 100644 --- a/src/components/SelectionList/components/Footer.tsx +++ b/src/components/SelectionList/components/Footer.tsx @@ -12,7 +12,14 @@ type FooterProps = { function Footer({footerContent, confirmButtonOptions, addBottomSafeAreaPadding = false}: FooterProps) { const styles = useThemeStyles(); - const {showButton: showConfirmButton, text: confirmButtonText, onConfirm, style: confirmButtonStyle, isDisabled: isConfirmButtonDisabled} = confirmButtonOptions ?? {}; + const { + showButton: showConfirmButton, + text: confirmButtonText, + onConfirm, + style: confirmButtonStyle, + isDisabled: isConfirmButtonDisabled, + confirmButtonSize = 'large', + } = confirmButtonOptions ?? {}; if (footerContent) { return ( ({footerContent, confirmButtonOptions, ad >