diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 3408ffbc4803..50b84ae68469 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,21 +1,39 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; +import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import * as SearchUtils from '@libs/SearchUtils'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {SearchContext, SelectedTransactions} from './types'; const defaultSearchContext = { currentSearchHash: -1, selectedTransactions: {}, + selectedReports: [], setCurrentSearchHash: () => {}, setSelectedTransactions: () => {}, clearSelectedTransactions: () => {}, + shouldShowStatusBarLoading: false, + setShouldShowStatusBarLoading: () => {}, }; const Context = React.createContext(defaultSearchContext); +function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[], selectedTransactions: SelectedTransactions) { + return (data ?? []) + .filter( + (item) => + !SearchUtils.isTransactionListItemType(item) && + !SearchUtils.isReportActionListItemType(item) && + item.reportID && + item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), + ) + .map((item) => item.reportID); +} + function SearchContextProvider({children}: ChildrenProps) { - const [searchContextData, setSearchContextData] = useState>({ + const [searchContextData, setSearchContextData] = useState>({ currentSearchHash: defaultSearchContext.currentSearchHash, selectedTransactions: defaultSearchContext.selectedTransactions, + selectedReports: defaultSearchContext.selectedReports, }); const setCurrentSearchHash = useCallback((searchHash: number) => { @@ -25,10 +43,14 @@ function SearchContextProvider({children}: ChildrenProps) { })); }, []); - const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions) => { + const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => { + // When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV. + const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions); + setSearchContextData((prevState) => ({ ...prevState, selectedTransactions, + selectedReports, })); }, []); @@ -40,19 +62,24 @@ function SearchContextProvider({children}: ChildrenProps) { setSearchContextData((prevState) => ({ ...prevState, selectedTransactions: {}, + selectedReports: [], })); }, [searchContextData.currentSearchHash], ); + const [shouldShowStatusBarLoading, setShouldShowStatusBarLoading] = useState(false); + const searchContext = useMemo( () => ({ ...searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions, + shouldShowStatusBarLoading, + setShouldShowStatusBarLoading, }), - [searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions], + [searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions, shouldShowStatusBarLoading], ); return {children}; diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 2580298ac3ac..ecca1f00e8ce 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,17 +1,18 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import Header from '@components/Header'; import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/types'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import {usePersonalDetails} from '@components/OnyxProvider'; -import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import Text from '@components/Text'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; @@ -29,7 +30,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {SearchDataTypes, SearchReport} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import {useSearchContext} from './SearchContext'; @@ -94,10 +95,6 @@ function HeaderWrapper({icon, title, subtitle, children, subtitleStyles = {}}: H type SearchPageHeaderProps = { queryJSON: SearchQueryJSON; hash: number; - onSelectDeleteOption?: (itemsToDelete: string[]) => void; - setOfflineModalOpen?: () => void; - setDownloadErrorModalOpen?: () => void; - data?: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]; }; type SearchHeaderOptionValue = DeepValueOf | undefined; @@ -121,37 +118,26 @@ function getHeaderContent(type: SearchDataTypes): HeaderContent { } } -function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, data}: SearchPageHeaderProps) { +function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {selectedTransactions} = useSearchContext(); + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const {selectedTransactions, clearSelectedTransactions, selectedReports} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const taxRates = getAllTaxRates(); const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST); + const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); + const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); - const selectedReports: Array = useMemo( - () => - (data ?? []) - .filter( - (item) => - !SearchUtils.isTransactionListItemType(item) && - !SearchUtils.isReportActionListItemType(item) && - item.reportID && - item.transactions.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), - ) - .map((item) => item.reportID), - [data, selectedTransactions], - ); const {status, type} = queryJSON; - const isCannedQuery = SearchUtils.isCannedSearchQuery(queryJSON); const headerSubtitle = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates); @@ -159,6 +145,15 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa const headerIcon = isCannedQuery ? getHeaderContent(type).icon : Illustrations.Filters; const subtitleStyles = isCannedQuery ? styles.textHeadlineH2 : {}; + const handleDeleteExpenses = () => { + if (selectedTransactionsKeys.length === 0) { + return; + } + + clearSelectedTransactions(); + setIsDeleteExpensesConfirmModalVisible(false); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); + }; const headerButtonsOptions = useMemo(() => { if (selectedTransactionsKeys.length === 0) { @@ -174,7 +169,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setIsOfflineModalVisible(true); return; } @@ -182,7 +177,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa SearchActions.exportSearchItemsToCSV( {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { - setDownloadErrorModalOpen?.(); + setIsDownloadErrorModalVisible(true); }, ); }, @@ -198,7 +193,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setIsOfflineModalVisible(true); return; } @@ -217,7 +212,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setIsOfflineModalVisible(true); return; } @@ -236,11 +231,10 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setIsOfflineModalVisible(true); return; } - - onSelectDeleteOption?.(selectedTransactionsKeys); + setIsDeleteExpensesConfirmModalVisible(true); }, }); } @@ -270,14 +264,11 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa selectedTransactionsKeys, selectedTransactions, translate, - onSelectDeleteOption, hash, theme.icon, styles.colorMuted, styles.fontWeightNormal, isOffline, - setOfflineModalOpen, - setDownloadErrorModalOpen, activeWorkspaceID, selectedReports, styles.textWrap, @@ -286,10 +277,42 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa if (shouldUseNarrowLayout) { if (selectionMode?.isEnabled) { return ( - + + + { + setIsDeleteExpensesConfirmModalVisible(false); + }} + title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})} + prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + setIsOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isOfflineModalVisible} + onClose={() => setIsOfflineModalVisible(false)} + /> + setIsDownloadErrorModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isDownloadErrorModalVisible} + onClose={() => setIsDownloadErrorModalVisible(false)} + /> + ); } return null; @@ -304,34 +327,66 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa const displaySearchRouter = SearchUtils.isCannedSearchQuery(queryJSON); return ( - - <> - {headerButtonsOptions.length > 0 ? ( - null} - shouldAlwaysShowDropdownMenu - pressOnEnter - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedTransactionsKeys.length})} - options={headerButtonsOptions} - isSplitButton={false} - shouldUseStyleUtilityForAnchorPosition - /> - ) : ( -