From d7e9dec86ba8f9891e31530e969dfc1f622e6306 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 29 Jul 2024 13:48:17 +0200 Subject: [PATCH 1/3] Move selectedTransactions to Search Context --- src/components/Search/SearchContext.tsx | 55 ++-- .../Search/SearchListWithHeader.tsx | 204 ------------ src/components/Search/SearchPageHeader.tsx | 43 +-- src/components/Search/index.tsx | 295 ++++++++++++++---- src/components/Search/types.ts | 5 +- src/pages/Search/SearchHoldReasonPage.tsx | 4 +- src/pages/Search/SearchPageBottomTab.tsx | 7 +- 7 files changed, 302 insertions(+), 311 deletions(-) delete mode 100644 src/components/Search/SearchListWithHeader.tsx diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 367a03e081cd..afe56c6335be 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,49 +1,58 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import type {SearchContext} from './types'; +import type {SearchContext, SelectedTransactions} from './types'; const defaultSearchContext = { currentSearchHash: -1, - selectedTransactionIDs: [], + selectedTransactions: {}, setCurrentSearchHash: () => {}, - setSelectedTransactionIDs: () => {}, + setSelectedTransactions: () => {}, + clearSelectedTransactions: () => {}, }; const Context = React.createContext(defaultSearchContext); function SearchContextProvider({children}: ChildrenProps) { - const [searchContextData, setSearchContextData] = useState>({ + const [searchContextData, setSearchContextData] = useState>({ currentSearchHash: defaultSearchContext.currentSearchHash, - selectedTransactionIDs: defaultSearchContext.selectedTransactionIDs, + selectedTransactions: defaultSearchContext.selectedTransactions, }); - const setCurrentSearchHash = useCallback( - (searchHash: number) => { - setSearchContextData({ - ...searchContextData, - currentSearchHash: searchHash, - }); - }, - [searchContextData], - ); + const setCurrentSearchHash = useCallback((searchHash: number) => { + setSearchContextData((prevState) => ({ + ...prevState, + currentSearchHash: searchHash, + })); + }, []); + + const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions) => { + setSearchContextData((prevState) => ({ + ...prevState, + selectedTransactions, + })); + }, []); - const setSelectedTransactionIDs = useCallback( - (selectedTransactionIDs: string[]) => { - setSearchContextData({ - ...searchContextData, - selectedTransactionIDs, - }); + const clearSelectedTransactions = useCallback( + (searchHash?: number) => { + if (!searchHash || searchHash === searchContextData.currentSearchHash) { + return; + } + setSearchContextData((prevState) => ({ + ...prevState, + selectedTransactions: {}, + })); }, - [searchContextData], + [searchContextData.currentSearchHash], ); const searchContext = useMemo( () => ({ ...searchContextData, setCurrentSearchHash, - setSelectedTransactionIDs, + setSelectedTransactions, + clearSelectedTransactions, }), - [searchContextData, setCurrentSearchHash, setSelectedTransactionIDs], + [searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions], ); return {children}; diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx deleted file mode 100644 index abb1bc45aebe..000000000000 --- a/src/components/Search/SearchListWithHeader.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react'; -import ConfirmModal from '@components/ConfirmModal'; -import DecisionModal from '@components/DecisionModal'; -import type {BaseSelectionListProps, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types'; -import SelectionListWithModal from '@components/SelectionListWithModal'; -import useLocalize from '@hooks/useLocalize'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as SearchActions from '@libs/actions/Search'; -import * as SearchUtils from '@libs/SearchUtils'; -import CONST from '@src/CONST'; -import type {SearchDataTypes, SearchReport} from '@src/types/onyx/SearchResults'; -import SearchPageHeader from './SearchPageHeader'; -import type {SearchStatus, SelectedTransactionInfo, SelectedTransactions} from './types'; - -type SearchListWithHeaderProps = Omit, 'onSelectAll' | 'onCheckboxPress' | 'sections'> & { - status: SearchStatus; - hash: number; - data: TransactionListItemType[] | ReportListItemType[]; - searchType: SearchDataTypes; -}; - -function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] { - return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}]; -} - -function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) { - return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected}; -} - -function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) { - return SearchUtils.isTransactionListItemType(item) - ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions) - : { - ...item, - transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)), - isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected), - }; -} - -function SearchListWithHeader({ListItem, onSelectRow, status, hash, data, searchType, ...props}: SearchListWithHeaderProps, ref: ForwardedRef) { - const {isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); - const [selectedTransactions, setSelectedTransactions] = useState({}); - const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); - const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); - const [offlineModalVisible, setOfflineModalVisible] = useState(false); - const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); - - const selectedReports: Array = useMemo(() => { - if (searchType !== CONST.SEARCH.DATA_TYPES.REPORT) { - return []; - } - - return data - .filter( - (item) => !SearchUtils.isTransactionListItemType(item) && item.reportID && item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected), - ) - .map((item) => item.reportID); - }, [selectedTransactions, data, searchType]); - - const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { - setSelectedTransactionsToDelete(itemsToDelete); - setDeleteExpensesConfirmModalVisible(true); - }; - - const handleOnCancelConfirmModal = () => { - setSelectedTransactionsToDelete([]); - setDeleteExpensesConfirmModalVisible(false); - }; - - const clearSelectedItems = () => setSelectedTransactions({}); - - const handleDeleteExpenses = () => { - if (selectedTransactionsToDelete.length === 0) { - return; - } - - clearSelectedItems(); - setDeleteExpensesConfirmModalVisible(false); - SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete); - }; - - useEffect(() => { - clearSelectedItems(); - }, [hash]); - - const toggleTransaction = useCallback( - (item: TransactionListItemType | ReportListItemType) => { - if (SearchUtils.isTransactionListItemType(item)) { - if (!item.keyForList) { - return; - } - - setSelectedTransactions((prev) => { - if (prev[item.keyForList]?.isSelected) { - const {[item.keyForList]: omittedTransaction, ...transactions} = prev; - return transactions; - } - return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}}; - }); - - return; - } - - if (item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) { - const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; - - item.transactions.forEach((transaction) => { - delete reducedSelectedTransactions[transaction.keyForList]; - }); - - setSelectedTransactions(reducedSelectedTransactions); - return; - } - - setSelectedTransactions({ - ...selectedTransactions, - ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)), - }); - }, - [selectedTransactions], - ); - - const toggleAllTransactions = () => { - const areItemsOfReportType = searchType === CONST.SEARCH.DATA_TYPES.REPORT; - const flattenedItems = areItemsOfReportType ? (data as ReportListItemType[]).flatMap((item) => item.transactions) : data; - const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; - - if (isAllSelected) { - clearSelectedItems(); - return; - } - - if (areItemsOfReportType) { - setSelectedTransactions(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry)))); - - return; - } - - setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry))); - }; - - const sortedSelectedData = useMemo(() => data.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions)), [data, selectedTransactions]); - - return ( - <> - setOfflineModalVisible(true)} - setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} - /> - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - sections={[{data: sortedSelectedData, isDisabled: false}]} - ListItem={ListItem} - onSelectRow={onSelectRow} - turnOnSelectionModeOnLongPress - onTurnOnSelectionMode={(item) => item && toggleTransaction(item)} - ref={ref} - onCheckboxPress={toggleTransaction} - onSelectAll={toggleAllTransactions} - /> - - setOfflineModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={offlineModalVisible} - onClose={() => setOfflineModalVisible(false)} - /> - setDownloadErrorModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={downloadErrorModalVisible} - onClose={() => setDownloadErrorModalVisible(false)} - /> - - ); -} - -SearchListWithHeader.displayName = 'SearchListWithHeader'; - -export default forwardRef(SearchListWithHeader); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 0dd83853456d..024d9f4163e6 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -5,6 +5,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -14,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as SearchActions from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; +import * as SearchUtils from '@libs/SearchUtils'; import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -23,38 +25,27 @@ import type {SearchReport} 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'; -import type {SearchStatus, SelectedTransactions} from './types'; +import type {SearchStatus} from './types'; type SearchPageHeaderProps = { status: SearchStatus; - selectedTransactions?: SelectedTransactions; - selectedReports?: Array; - clearSelectedItems?: () => void; hash: number; onSelectDeleteOption?: (itemsToDelete: string[]) => void; setOfflineModalOpen?: () => void; setDownloadErrorModalOpen?: () => void; + data?: TransactionListItemType[] | ReportListItemType[]; }; type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({ - status, - selectedTransactions = {}, - hash, - clearSelectedItems, - onSelectDeleteOption, - setOfflineModalOpen, - setDownloadErrorModalOpen, - selectedReports, -}: SearchPageHeaderProps) { +function SearchPageHeader({status, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, data}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); const {isSmallScreenWidth} = useResponsiveLayout(); - const {setSelectedTransactionIDs} = useSearchContext(); + const {selectedTransactions, clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); const headerContent: {[key in SearchStatus]: {icon: IconAsset; title: string}} = { @@ -64,7 +55,20 @@ function SearchPageHeader({ finished: {icon: Illustrations.CheckmarkCircle, title: translate('common.finished')}, }; - const selectedTransactionsKeys = Object.keys(selectedTransactions ?? []); + const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); + + const selectedReports: Array = useMemo( + () => + (data ?? []) + .filter( + (item) => + !SearchUtils.isTransactionListItemType(item) && + item.reportID && + item.transactions.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), + ) + .map((item) => item.reportID), + [data, selectedTransactions], + ); const headerButtonsOptions = useMemo(() => { if (selectedTransactionsKeys.length === 0) { @@ -105,11 +109,9 @@ function SearchPageHeader({ return; } - clearSelectedItems?.(); if (selectionMode?.isEnabled) { turnOffMobileSelectionMode(); } - setSelectedTransactionIDs(selectedTransactionsKeys); Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP); }, }); @@ -129,7 +131,7 @@ function SearchPageHeader({ return; } - clearSelectedItems?.(); + clearSelectedTransactions(); if (selectionMode?.isEnabled) { turnOffMobileSelectionMode(); } @@ -182,7 +184,7 @@ function SearchPageHeader({ selectedTransactions, translate, onSelectDeleteOption, - clearSelectedItems, + clearSelectedTransactions, hash, theme.icon, styles.colorMuted, @@ -193,7 +195,6 @@ function SearchPageHeader({ activeWorkspaceID, selectedReports, styles.textWrap, - setSelectedTransactionIDs, selectionMode?.isEnabled, ]); diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 5eba0ca81844..8d3a5524ea5b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,14 +1,19 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import SelectionListWithModal from '@components/SelectionListWithModal'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -23,11 +28,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; -import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import {useSearchContext} from './SearchContext'; -import SearchListWithHeader from './SearchListWithHeader'; import SearchPageHeader from './SearchPageHeader'; -import type {SearchColumnType, SearchQueryJSON, SearchStatus, SortOrder} from './types'; +import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; type SearchProps = { queryJSON: SearchQueryJSON; @@ -39,20 +42,110 @@ const transactionItemMobileHeight = 100; const reportItemTransactionHeight = 52; const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item const searchHeaderHeight = 54; + +function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] { + return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}]; +} + +function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) { + return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected}; +} + +function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) { + return SearchUtils.isTransactionListItemType(item) + ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions) + : { + ...item, + transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)), + isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected), + }; +} + +function prepareTransactionsList(item: TransactionListItemType, selectedTransactions: SelectedTransactions) { + if (selectedTransactions[item.keyForList]?.isSelected) { + const {[item.keyForList]: omittedTransaction, ...transactions} = selectedTransactions; + + return transactions; + } + + return {...selectedTransactions, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}}; +} + function Search({queryJSON, policyIDs}: SearchProps) { const {isOffline} = useNetwork(); + const {translate} = useLocalize(); const styles = useThemeStyles(); const {isLargeScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); - const {setCurrentSearchHash} = useSearchContext(); + const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); - const [offset, setOffset] = React.useState(0); + const [offset, setOffset] = useState(0); + const [offlineModalVisible, setOfflineModalVisible] = useState(false); + const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); + const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); + const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const {status, sortBy, sortOrder, hash} = queryJSON; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + useEffect(() => { + if (isSmallScreenWidth) { + return; + } + clearSelectedTransactions(hash); + setCurrentSearchHash(hash); + + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [hash]); + + useEffect(() => { + const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); + if (!isSmallScreenWidth) { + if (selectedKeys.length === 0) { + turnOffMobileSelectionMode(); + } + return; + } + if (selectedKeys.length > 0 && !selectionMode?.isEnabled) { + turnOnMobileSelectionMode(); + } + }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); + + const handleOnCancelConfirmModal = () => { + setSelectedTransactionsToDelete([]); + setDeleteExpensesConfirmModalVisible(false); + }; + + const handleDeleteExpenses = () => { + if (selectedTransactionsToDelete.length === 0) { + return; + } + + clearSelectedTransactions(); + setDeleteExpensesConfirmModalVisible(false); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete); + }; + + const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { + setSelectedTransactionsToDelete(itemsToDelete); + setDeleteExpensesConfirmModalVisible(true); + }; + + useEffect(() => { + const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); + if (!isSmallScreenWidth) { + if (selectedKeys.length === 0) { + turnOffMobileSelectionMode(); + } + return; + } + if (selectedKeys.length > 0 && !selectionMode?.isEnabled) { + turnOnMobileSelectionMode(); + } + }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); + const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType) => { if (SearchUtils.isTransactionListItemType(item)) { @@ -94,8 +187,6 @@ function Search({queryJSON, policyIDs}: SearchProps) { return; } - setCurrentSearchHash(hash); - SearchActions.search({hash, query: status, policyIDs, offset, sortBy, sortOrder}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [hash, isOffline, offset]); @@ -104,6 +195,33 @@ function Search({queryJSON, policyIDs}: SearchProps) { const shouldShowLoadingState = !isOffline && !isDataLoaded; const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; + const toggleTransaction = (item: TransactionListItemType | ReportListItemType) => { + if (SearchUtils.isTransactionListItemType(item)) { + if (!item.keyForList) { + return; + } + + setSelectedTransactions(prepareTransactionsList(item, selectedTransactions)); + return; + } + + if (item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) { + const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; + + item.transactions.forEach((transaction) => { + delete reducedSelectedTransactions[transaction.keyForList]; + }); + + setSelectedTransactions(reducedSelectedTransactions); + return; + } + + setSelectedTransactions({ + ...selectedTransactions, + ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)), + }); + }; + if (shouldShowLoadingState) { return ( <> @@ -163,7 +281,26 @@ function Search({queryJSON, policyIDs}: SearchProps) { const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections(searchResults?.data ?? {}, searchResults?.search ?? {}, type); - const sortedData = SearchUtils.getSortedSections(type, data, sortBy, sortOrder); + const sortedSelectedData = data.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions)); + + const toggleAllTransactions = () => { + const areItemsOfReportType = searchResults?.search.type === CONST.SEARCH.DATA_TYPES.REPORT; + const flattenedItems = areItemsOfReportType ? (data as ReportListItemType[]).flatMap((item) => item.transactions) : data; + const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; + + if (isAllSelected) { + clearSelectedTransactions(); + return; + } + + if (areItemsOfReportType) { + setSelectedTransactions(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry)))); + + return; + } + + setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry))); + }; const onSortPress = (column: SearchColumnType, order: SortOrder) => { const currentSearchParams = SearchUtils.getCurrentSearchParams(); @@ -180,56 +317,96 @@ function Search({queryJSON, policyIDs}: SearchProps) { const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true; return ( - - ) - } - canSelectMultiple={canSelectMultiple} - customListHeaderHeight={searchHeaderHeight} - // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, - // we have configured a larger windowSize and a longer delay between batch renders. - // The windowSize determines the number of items rendered before and after the currently visible items. - // A larger windowSize helps pre-render more items, reducing the likelihood of blank spaces appearing. - // The updateCellsBatchingPeriod sets the delay (in milliseconds) between rendering batches of cells. - // A longer delay allows the UI to handle rendering in smaller increments, which can improve performance and smoothness. - // For more information, refer to the React Native documentation: - // https://reactnative.dev/docs/0.73/optimizing-flatlist-configuration#windowsize - // https://reactnative.dev/docs/0.73/optimizing-flatlist-configuration#updatecellsbatchingperiod - windowSize={111} - updateCellsBatchingPeriod={200} - ListItem={ListItem} - onSelectRow={openReport} - getItemHeight={getItemHeightMemoized} - shouldDebounceRowSelect - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]} - containerStyle={[styles.pv0]} - showScrollIndicator={false} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - listFooterContent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - /> + <> + setOfflineModalVisible(true)} + setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} + /> + + + sections={[{data: sortedSelectedData, isDisabled: false}]} + turnOnSelectionModeOnLongPress + onTurnOnSelectionMode={(item) => item && toggleTransaction(item)} + onCheckboxPress={toggleTransaction} + onSelectAll={toggleAllTransactions} + customListHeader={ + !isLargeScreenWidth ? null : ( + + ) + } + canSelectMultiple={canSelectMultiple} + customListHeaderHeight={searchHeaderHeight} + // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, + // we have configured a larger windowSize and a longer delay between batch renders. + // The windowSize determines the number of items rendered before and after the currently visible items. + // A larger windowSize helps pre-render more items, reducing the likelihood of blank spaces appearing. + // The updateCellsBatchingPeriod sets the delay (in milliseconds) between rendering batches of cells. + // A longer delay allows the UI to handle rendering in smaller increments, which can improve performance and smoothness. + // For more information, refer to the React Native documentation: + // https://reactnative.dev/docs/0.73/optimizing-flatlist-configuration#windowsize + // https://reactnative.dev/docs/0.73/optimizing-flatlist-configuration#updatecellsbatchingperiod + windowSize={111} + updateCellsBatchingPeriod={200} + ListItem={ListItem} + onSelectRow={openReport} + getItemHeight={getItemHeightMemoized} + shouldDebounceRowSelect + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]} + containerStyle={[styles.pv0]} + showScrollIndicator={false} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + listFooterContent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + /> + + setOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={offlineModalVisible} + onClose={() => setOfflineModalVisible(false)} + /> + setDownloadErrorModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={downloadErrorModalVisible} + onClose={() => setDownloadErrorModalVisible(false)} + /> + ); } diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index cf8a2eb04e14..e8224747cc85 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -28,9 +28,10 @@ type SearchStatus = ValueOf; type SearchContext = { currentSearchHash: number; - selectedTransactionIDs: string[]; + selectedTransactions: SelectedTransactions; setCurrentSearchHash: (hash: number) => void; - setSelectedTransactionIDs: (selectedTransactionIds: string[]) => void; + setSelectedTransactions: (selectedTransactions: SelectedTransactions) => void; + clearSelectedTransactions: (hash?: number) => void; }; type ASTNode = { diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index d8d11662e34a..7ea22421737d 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -25,11 +25,13 @@ type SearchHoldReasonPageProps = { function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); - const {currentSearchHash, selectedTransactionIDs} = useSearchContext(); + const {currentSearchHash, selectedTransactions, clearSelectedTransactions} = useSearchContext(); const {backTo = ''} = route.params ?? {}; + const selectedTransactionIDs = Object.keys(selectedTransactions); const onSubmit = (values: FormOnyxValues) => { SearchActions.holdMoneyRequestOnSearch(currentSearchHash, selectedTransactionIDs, values.comment); + clearSelectedTransactions(); Navigation.goBack(); }; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 4f3138aef110..c4e7d19c769a 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -4,6 +4,7 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; +import {useSearchContext} from '@components/Search/SearchContext'; import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -24,6 +25,7 @@ function SearchPageBottomTab() { const {shouldUseNarrowLayout} = useResponsiveLayout(); const activeCentralPaneRoute = useActiveCentralPaneRoute(); const styles = useThemeStyles(); + const {clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); const {queryJSON, policyIDs} = useMemo(() => { @@ -65,7 +67,10 @@ function SearchPageBottomTab() { ) : ( { + clearSelectedTransactions(); + turnOffMobileSelectionMode(); + }} /> )} {shouldUseNarrowLayout && queryJSON && ( From 16547b789865924524ef4ff955d50327570a4468 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 29 Jul 2024 13:56:06 +0200 Subject: [PATCH 2/3] Remove redundant condition --- src/components/Search/SearchContext.tsx | 2 +- src/components/Search/index.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index afe56c6335be..3408ffbc4803 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -34,7 +34,7 @@ function SearchContextProvider({children}: ChildrenProps) { const clearSelectedTransactions = useCallback( (searchHash?: number) => { - if (!searchHash || searchHash === searchContextData.currentSearchHash) { + if (searchHash === searchContextData.currentSearchHash) { return; } setSearchContextData((prevState) => ({ diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 8d3a5524ea5b..7bf3f0f5b047 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -289,6 +289,7 @@ function Search({queryJSON, policyIDs}: SearchProps) { const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; if (isAllSelected) { + console.log('teraz'); clearSelectedTransactions(); return; } From 243435b50e95a1a63ade16cac95644e3aac573a5 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 29 Jul 2024 14:04:37 +0200 Subject: [PATCH 3/3] Remove console.log --- src/components/Search/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 7bf3f0f5b047..8d3a5524ea5b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -289,7 +289,6 @@ function Search({queryJSON, policyIDs}: SearchProps) { const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; if (isAllSelected) { - console.log('teraz'); clearSelectedTransactions(); return; }