11/* eslint-disable rulesdir/prefer-early-return */
2- import { useFocusEffect , useIsFocused , useRoute } from '@react-navigation/native' ;
2+ import { useIsFocused , useRoute } from '@react-navigation/native' ;
33import { isUserValidatedSelector } from '@selectors/Account' ;
44import { tierNameSelector } from '@selectors/UserWallet' ;
55import isEmpty from 'lodash/isEmpty' ;
66import React , { useCallback , useContext , useEffect , useLayoutEffect , useMemo , useRef , useState } from 'react' ;
77import type { LayoutChangeEvent , ListRenderItemInfo , NativeScrollEvent , NativeSyntheticEvent } from 'react-native' ;
88import { DeviceEventEmitter , InteractionManager , View } from 'react-native' ;
9- import type { ValueOf } from 'type-fest' ;
10- import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu' ;
11- import Checkbox from '@components/Checkbox' ;
12- import DecisionModal from '@components/DecisionModal' ;
13- import { useDelegateNoAccessActions , useDelegateNoAccessState } from '@components/DelegateNoAccessModalProvider' ;
149import FlatListWithScrollKey from '@components/FlatList/FlatListWithScrollKey' ;
15- import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal' ;
16- import { ModalActions } from '@components/Modal/Global/ModalContext' ;
17- import OfflineWithFeedback from '@components/OfflineWithFeedback' ;
1810import { usePersonalDetails } from '@components/OnyxListItemProvider' ;
19- import { PressableWithFeedback } from '@components/Pressable' ;
2011import ScrollView from '@components/ScrollView' ;
21- import { useSearchActionsContext , useSearchStateContext } from '@components/Search/SearchContext' ;
22- import Text from '@components/Text' ;
23- import useConfirmModal from '@hooks/useConfirmModal' ;
2412import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' ;
25- import useFilterSelectedTransactions from '@hooks/useFilterSelectedTransactions' ;
2613import useLoadReportActions from '@hooks/useLoadReportActions' ;
2714import useLocalize from '@hooks/useLocalize' ;
28- import useMobileSelectionMode from '@hooks/useMobileSelectionMode' ;
2915import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus' ;
3016import useNewTransactions from '@hooks/useNewTransactions' ;
3117import useOnyx from '@hooks/useOnyx' ;
@@ -37,11 +23,8 @@ import useReportScrollManager from '@hooks/useReportScrollManager';
3723import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection' ;
3824import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP' ;
3925import useScrollToEndOnNewMessageReceived from '@hooks/useScrollToEndOnNewMessageReceived' ;
40- import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions' ;
4126import useThemeStyles from '@hooks/useThemeStyles' ;
4227import useWindowDimensions from '@hooks/useWindowDimensions' ;
43- import { dismissRejectUseExplanation } from '@libs/actions/IOU' ;
44- import { queueExportSearchWithTemplate } from '@libs/actions/Search' ;
4528import { isConsecutiveChronosAutomaticTimerAction } from '@libs/ChronosUtils' ;
4629import DateUtils from '@libs/DateUtils' ;
4730import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID' ;
@@ -61,19 +44,9 @@ import {
6144 isReportActionVisible ,
6245 wasMessageReceivedWhileOffline ,
6346} from '@libs/ReportActionsUtils' ;
64- import {
65- canUserPerformWriteAction ,
66- chatIncludesChronosWithID ,
67- getOriginalReportID ,
68- getReportLastVisibleActionCreated ,
69- getReportOfflinePendingActionAndErrors ,
70- isHarvestCreatedExpenseReport ,
71- isUnread ,
72- } from '@libs/ReportUtils' ;
73- import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView' ;
47+ import { canUserPerformWriteAction , chatIncludesChronosWithID , getOriginalReportID , getReportLastVisibleActionCreated , isHarvestCreatedExpenseReport , isUnread } from '@libs/ReportUtils' ;
7448import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd' ;
7549import type { SkeletonSpanReasonAttributes } from '@libs/telemetry/useSkeletonSpan' ;
76- import { isTransactionPendingDelete } from '@libs/TransactionUtils' ;
7750import Visibility from '@libs/Visibility' ;
7851import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute' ;
7952import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter' ;
@@ -86,14 +59,13 @@ import variables from '@styles/variables';
8659import { getOlderActions , openReport , readNewestAction , subscribeToNewActionEvent } from '@userActions/Report' ;
8760import CONST from '@src/CONST' ;
8861import ONYXKEYS from '@src/ONYXKEYS' ;
89- import ROUTES from '@src/ROUTES' ;
90- import type { Route } from '@src/ROUTES' ;
9162import type SCREENS from '@src/SCREENS' ;
9263import type * as OnyxTypes from '@src/types/onyx' ;
9364import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList' ;
9465import MoneyRequestViewReportFields from './MoneyRequestViewReportFields' ;
9566import ReportActionsListLoadingSkeleton from './ReportActionsListLoadingSkeleton' ;
9667import SearchMoneyRequestReportEmptyState from './SearchMoneyRequestReportEmptyState' ;
68+ import SelectionToolbar from './SelectionToolbar' ;
9769
9870/**
9971 * In this view we are not handling the special single transaction case, we're just handling the report
@@ -148,8 +120,6 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
148120 ) ;
149121 const newTransactions = useNewTransactions ( reportMetadata ?. hasOnceLoadedReportActions , reportTransactions ) ;
150122 const showReportActionsLoadingState = reportMetadata ?. isLoadingInitialReportActions && ! reportMetadata ?. hasOnceLoadedReportActions ;
151- const { reportPendingAction} = getReportOfflinePendingActionAndErrors ( report ) ;
152-
153123 const reportTransactionIDs = useMemo ( ( ) => transactions . map ( ( transaction ) => transaction . transactionID ) , [ transactions ] ) ;
154124 const [ chatReport ] = useOnyx ( `${ ONYXKEYS . COLLECTION . REPORT } ${ getNonEmptyStringOnyxID ( report ?. chatReportID ) } ` ) ;
155125
@@ -169,10 +139,6 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
169139 const isTryNewDotNVPDismissed = ! ! tryNewDot ?. classicRedirect ?. dismissed ;
170140 const [ introSelected ] = useOnyx ( ONYXKEYS . NVP_INTRO_SELECTED ) ;
171141 const [ betas ] = useOnyx ( ONYXKEYS . BETAS ) ;
172- const { isDelegateAccessRestricted} = useDelegateNoAccessState ( ) ;
173- const { showDelegateNoAccessModal} = useDelegateNoAccessActions ( ) ;
174-
175- const transactionsWithoutPendingDelete = useMemo ( ( ) => transactions . filter ( ( t ) => ! isTransactionPendingDelete ( t ) ) , [ transactions ] ) ;
176142 // reportActions is passed as an array because it's sorted chronologically for FlatList rendering and pagination.
177143 // However, getOriginalReportID expects the Onyx object format (keyed by reportActionID) for efficient lookups.
178144 const reportActionsObject = useMemo ( ( ) => {
@@ -193,149 +159,11 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
193159
194160 const { shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP ( ) ;
195161
196- const [ session ] = useOnyx ( ONYXKEYS . SESSION ) ;
197162 const [ reportNameValuePairs ] = useOnyx ( `${ ONYXKEYS . COLLECTION . REPORT_NAME_VALUE_PAIRS } ${ getNonEmptyStringOnyxID ( reportID ) } ` ) ;
198163 const shouldShowHarvestCreatedAction = isHarvestCreatedExpenseReport ( reportNameValuePairs ?. origin , reportNameValuePairs ?. originalID ) ;
199- const [ offlineModalVisible , setOfflineModalVisible ] = useState ( false ) ;
200- const [ isDownloadErrorModalVisible , setIsDownloadErrorModalVisible ] = useState ( false ) ;
201164 const [ enableScrollToEnd , setEnableScrollToEnd ] = useState < boolean > ( false ) ;
202165 const [ lastActionEventId , setLastActionEventId ] = useState < string > ( '' ) ;
203166
204- const { selectedTransactionIDs, currentSelectedTransactionReportID} = useSearchStateContext ( ) ;
205- const { setSelectedTransactions, clearSelectedTransactions, setCurrentSelectedTransactionReportID} = useSearchActionsContext ( ) ;
206-
207- useFocusEffect (
208- useCallback ( ( ) => {
209- if ( reportID && currentSelectedTransactionReportID !== reportID && selectedTransactionIDs . length > 0 ) {
210- clearSelectedTransactions ( true ) ;
211- }
212-
213- setCurrentSelectedTransactionReportID ( reportID ) ;
214- } , [ clearSelectedTransactions , currentSelectedTransactionReportID , reportID , selectedTransactionIDs . length , setCurrentSelectedTransactionReportID ] ) ,
215- ) ;
216-
217- useFilterSelectedTransactions ( transactions , reportID ) ;
218-
219- const isMobileSelectionModeEnabled = useMobileSelectionMode ( ) ;
220- const { showConfirmModal} = useConfirmModal ( ) ;
221- const beginExportWithTemplate = useCallback (
222- ( templateName : string , templateType : string , transactionIDList : string [ ] ) => {
223- if ( isOffline ) {
224- setOfflineModalVisible ( true ) ;
225- return ;
226- }
227-
228- if ( ! report ) {
229- return ;
230- }
231-
232- queueExportSearchWithTemplate ( {
233- templateName,
234- templateType,
235- jsonQuery : '{}' ,
236- reportIDList : [ report . reportID ] ,
237- transactionIDList,
238- policyID : policy ?. id ,
239- } ) ;
240-
241- showConfirmModal ( {
242- title : translate ( 'export.exportInProgress' ) ,
243- prompt : translate ( 'export.conciergeWillSend' ) ,
244- confirmText : translate ( 'common.buttonConfirm' ) ,
245- shouldShowCancelButton : false ,
246- } ) . then ( ( result ) => {
247- if ( result . action === ModalActions . CONFIRM ) {
248- clearSelectedTransactions ( undefined , true ) ;
249- }
250- } ) ;
251- } ,
252- [ isOffline , report , policy ?. id , showConfirmModal , translate , clearSelectedTransactions ] ,
253- ) ;
254-
255- const onDeleteSelected = useCallback (
256- ( handleDeleteTransactions : ( ) => void , handleDeleteTransactionsWithNavigation : ( backToRoute ?: Route ) => void ) => {
257- showConfirmModal ( {
258- title : translate ( 'iou.deleteExpense' , {
259- count : selectedTransactionIDs . length ,
260- } ) ,
261- prompt : translate ( 'iou.deleteConfirmation' , {
262- count : selectedTransactionIDs . length ,
263- } ) ,
264- confirmText : translate ( 'common.delete' ) ,
265- cancelText : translate ( 'common.cancel' ) ,
266- danger : true ,
267- shouldEnableNewFocusManagement : true ,
268- } ) . then ( ( result ) => {
269- if ( result . action !== ModalActions . CONFIRM ) {
270- return ;
271- }
272- const shouldNavigateBack = transactions . filter ( ( trans ) => trans . pendingAction !== CONST . RED_BRICK_ROAD_PENDING_ACTION . DELETE ) . length === selectedTransactionIDs . length ;
273- if ( shouldNavigateBack ) {
274- const backToRoute = route . params ?. backTo ?? ( chatReport ?. reportID ? ROUTES . REPORT_WITH_ID . getRoute ( chatReport . reportID ) : undefined ) ;
275- handleDeleteTransactionsWithNavigation ( backToRoute ) ;
276- return ;
277- }
278- handleDeleteTransactions ( ) ;
279- } ) ;
280- } ,
281- [ showConfirmModal , translate , selectedTransactionIDs . length , transactions , route . params ?. backTo , chatReport ?. reportID ] ,
282- ) ;
283-
284- const { options : originalSelectedTransactionsOptions } = useSelectedTransactionsActions ( {
285- report,
286- reportActions,
287- allTransactionsLength : transactions . length ,
288- session,
289- onExportFailed : ( ) => setIsDownloadErrorModalVisible ( true ) ,
290- onExportOffline : ( ) => setOfflineModalVisible ( true ) ,
291- policy,
292- beginExportWithTemplate : ( templateName , templateType , transactionIDList ) => beginExportWithTemplate ( templateName , templateType , transactionIDList ) ,
293- onDeleteSelected,
294- } ) ;
295-
296- const [ dismissedRejectUseExplanation ] = useOnyx ( ONYXKEYS . NVP_DISMISSED_REJECT_USE_EXPLANATION ) ;
297-
298- const [ rejectModalAction , setRejectModalAction ] = useState < ValueOf < typeof CONST . REPORT . TRANSACTION_SECONDARY_ACTIONS . REJECT_BULK > | null > ( null ) ;
299-
300- const selectedTransactionsOptions = useMemo ( ( ) => {
301- return originalSelectedTransactionsOptions . map ( ( option ) => {
302- if ( option . value === CONST . REPORT . SECONDARY_ACTIONS . REJECT ) {
303- return {
304- ...option ,
305- onSelected : ( ) => {
306- if ( isDelegateAccessRestricted ) {
307- showDelegateNoAccessModal ( ) ;
308- return ;
309- }
310-
311- if ( dismissedRejectUseExplanation ) {
312- option . onSelected ?.( ) ;
313- } else {
314- setRejectModalAction ( CONST . REPORT . TRANSACTION_SECONDARY_ACTIONS . REJECT_BULK ) ;
315- }
316- } ,
317- } ;
318- }
319- return option ;
320- } ) ;
321- } , [ originalSelectedTransactionsOptions , dismissedRejectUseExplanation , isDelegateAccessRestricted , showDelegateNoAccessModal ] ) ;
322-
323- const popoverUseScrollView = shouldPopoverUseScrollView ( selectedTransactionsOptions ) ;
324-
325- const dismissRejectModalBasedOnAction = useCallback ( ( ) => {
326- if ( rejectModalAction === CONST . REPORT . TRANSACTION_SECONDARY_ACTIONS . REJECT_BULK ) {
327- dismissRejectUseExplanation ( ) ;
328- if ( report ?. reportID ) {
329- Navigation . navigate (
330- ROUTES . SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS . getRoute ( {
331- reportID : report . reportID ,
332- } ) ,
333- ) ;
334- }
335- }
336- setRejectModalAction ( null ) ;
337- } , [ rejectModalAction , report ?. reportID ] ) ;
338-
339167 // We are reversing actions because in this View we are starting at the top and don't use Inverted list
340168 const visibleReportActions = useMemo ( ( ) => {
341169 const filteredActions = reportActions . filter ( ( reportAction ) => {
@@ -820,7 +648,6 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
820648 markOpenReportEnd ( report , { warm : ! shouldShowOpenReportLoadingSkeleton } ) ;
821649 } , [ report , shouldShowOpenReportLoadingSkeleton ] ) ;
822650
823- const isSelectAllChecked = selectedTransactionIDs . length > 0 && selectedTransactionIDs . length === transactionsWithoutPendingDelete . length ;
824651 // Wrapped into useCallback to stabilize children re-renders
825652 const keyExtractor = useCallback ( ( item : OnyxTypes . ReportAction ) => item . reportActionID , [ ] ) ;
826653
@@ -844,52 +671,11 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
844671 style = { [ styles . flex1 ] }
845672 ref = { wrapperViewRef }
846673 >
847- { shouldUseNarrowLayout && isMobileSelectionModeEnabled && (
848- < OfflineWithFeedback pendingAction = { reportPendingAction } >
849- < ButtonWithDropdownMenu
850- onPress = { ( ) => null }
851- options = { selectedTransactionsOptions }
852- customText = { translate ( 'workspace.common.selected' , {
853- count : selectedTransactionIDs . length ,
854- } ) }
855- isSplitButton = { false }
856- shouldAlwaysShowDropdownMenu
857- shouldPopoverUseScrollView = { popoverUseScrollView }
858- wrapperStyle = { [ styles . w100 , styles . ph5 ] }
859- />
860- < View style = { [ styles . alignItemsCenter , styles . userSelectNone , styles . flexRow , styles . pt6 , styles . ph8 , styles . pb3 ] } >
861- < Checkbox
862- accessibilityLabel = { translate ( 'accessibilityHints.selectAllItems' ) }
863- isChecked = { isSelectAllChecked }
864- isIndeterminate = { selectedTransactionIDs . length > 0 && selectedTransactionIDs . length !== transactionsWithoutPendingDelete . length }
865- onPress = { ( ) => {
866- if ( selectedTransactionIDs . length !== 0 ) {
867- clearSelectedTransactions ( true ) ;
868- } else {
869- setSelectedTransactions ( transactionsWithoutPendingDelete . map ( ( t ) => t . transactionID ) ) ;
870- }
871- } }
872- />
873- < PressableWithFeedback
874- style = { [ styles . userSelectNone , styles . alignItemsCenter ] }
875- onPress = { ( ) => {
876- if ( isSelectAllChecked ) {
877- clearSelectedTransactions ( true ) ;
878- } else {
879- setSelectedTransactions ( transactionsWithoutPendingDelete . map ( ( t ) => t . transactionID ) ) ;
880- }
881- } }
882- accessibilityLabel = { translate ( 'accessibilityHints.selectAllItems' ) }
883- role = "button"
884- accessibilityState = { { checked : isSelectAllChecked } }
885- dataSet = { { [ CONST . SELECTION_SCRAPER_HIDDEN_ELEMENT ] : true } }
886- sentryLabel = { CONST . SENTRY_LABEL . REPORT . MONEY_REQUEST_REPORT_ACTIONS_LIST_SELECT_ALL }
887- >
888- < Text style = { [ styles . textStrong , styles . ph3 ] } > { translate ( 'workspace.people.selectAll' ) } </ Text >
889- </ PressableWithFeedback >
890- </ View >
891- </ OfflineWithFeedback >
892- ) }
674+ < SelectionToolbar
675+ reportID = { report . reportID }
676+ transactions = { transactions }
677+ reportActions = { reportActions }
678+ />
893679 < View style = { [ styles . flex1 , styles . justifyContentEnd , styles . overflowHidden ] } >
894680 < FloatingMessageCounter
895681 hasNewMessages = { ! ! unreadMarkerReportActionID }
@@ -953,30 +739,6 @@ function MoneyRequestReportActionsList({reportID: reportIDProp, onLayout}: Money
953739 />
954740 ) }
955741 </ View >
956- < DecisionModal
957- title = { translate ( 'common.downloadFailedTitle' ) }
958- prompt = { translate ( 'common.downloadFailedDescription' ) }
959- isSmallScreenWidth = { shouldUseNarrowLayout }
960- onSecondOptionSubmit = { ( ) => setIsDownloadErrorModalVisible ( false ) }
961- secondOptionText = { translate ( 'common.buttonConfirm' ) }
962- isVisible = { isDownloadErrorModalVisible }
963- onClose = { ( ) => setIsDownloadErrorModalVisible ( false ) }
964- />
965- < DecisionModal
966- title = { translate ( 'common.youAppearToBeOffline' ) }
967- prompt = { translate ( 'common.offlinePrompt' ) }
968- isSmallScreenWidth = { shouldUseNarrowLayout }
969- onSecondOptionSubmit = { ( ) => setOfflineModalVisible ( false ) }
970- secondOptionText = { translate ( 'common.buttonConfirm' ) }
971- isVisible = { offlineModalVisible }
972- onClose = { ( ) => setOfflineModalVisible ( false ) }
973- />
974- { ! ! rejectModalAction && (
975- < HoldOrRejectEducationalModal
976- onClose = { dismissRejectModalBasedOnAction }
977- onConfirm = { dismissRejectModalBasedOnAction }
978- />
979- ) }
980742 </ View >
981743 ) ;
982744}
0 commit comments