Skip to content

Commit 8db0230

Browse files
adhorodyskiclaude
andcommitted
Extract SelectionToolbar from MoneyRequestReportActionsList
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d1dbbcb commit 8db0230

2 files changed

Lines changed: 283 additions & 246 deletions

File tree

src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Lines changed: 8 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
/* eslint-disable rulesdir/prefer-early-return */
2-
import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native';
2+
import {useIsFocused, useRoute} from '@react-navigation/native';
33
import {isUserValidatedSelector} from '@selectors/Account';
44
import {tierNameSelector} from '@selectors/UserWallet';
55
import isEmpty from 'lodash/isEmpty';
66
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
77
import type {LayoutChangeEvent, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
88
import {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';
149
import 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';
1810
import {usePersonalDetails} from '@components/OnyxListItemProvider';
19-
import {PressableWithFeedback} from '@components/Pressable';
2011
import ScrollView from '@components/ScrollView';
21-
import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
22-
import Text from '@components/Text';
23-
import useConfirmModal from '@hooks/useConfirmModal';
2412
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
25-
import useFilterSelectedTransactions from '@hooks/useFilterSelectedTransactions';
2613
import useLoadReportActions from '@hooks/useLoadReportActions';
2714
import useLocalize from '@hooks/useLocalize';
28-
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
2915
import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus';
3016
import useNewTransactions from '@hooks/useNewTransactions';
3117
import useOnyx from '@hooks/useOnyx';
@@ -37,11 +23,8 @@ import useReportScrollManager from '@hooks/useReportScrollManager';
3723
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
3824
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
3925
import useScrollToEndOnNewMessageReceived from '@hooks/useScrollToEndOnNewMessageReceived';
40-
import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
4126
import useThemeStyles from '@hooks/useThemeStyles';
4227
import useWindowDimensions from '@hooks/useWindowDimensions';
43-
import {dismissRejectUseExplanation} from '@libs/actions/IOU';
44-
import {queueExportSearchWithTemplate} from '@libs/actions/Search';
4528
import {isConsecutiveChronosAutomaticTimerAction} from '@libs/ChronosUtils';
4629
import DateUtils from '@libs/DateUtils';
4730
import 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';
7448
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';
7549
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
76-
import {isTransactionPendingDelete} from '@libs/TransactionUtils';
7750
import Visibility from '@libs/Visibility';
7851
import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';
7952
import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
@@ -86,14 +59,13 @@ import variables from '@styles/variables';
8659
import {getOlderActions, openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report';
8760
import CONST from '@src/CONST';
8861
import ONYXKEYS from '@src/ONYXKEYS';
89-
import ROUTES from '@src/ROUTES';
90-
import type {Route} from '@src/ROUTES';
9162
import type SCREENS from '@src/SCREENS';
9263
import type * as OnyxTypes from '@src/types/onyx';
9364
import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList';
9465
import MoneyRequestViewReportFields from './MoneyRequestViewReportFields';
9566
import ReportActionsListLoadingSkeleton from './ReportActionsListLoadingSkeleton';
9667
import 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

Comments
 (0)