From 0cdffe10cf2f9d84023a714adca21b58dd376462 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 29 Aug 2024 13:51:47 +0200 Subject: [PATCH 01/33] add animation logic --- src/components/Search/SearchPageHeader.tsx | 1 + src/components/Search/index.tsx | 32 ++++++--- .../SelectionList/BaseSelectionList.tsx | 2 + src/components/SelectionList/index.tsx | 5 +- src/components/SelectionList/types.ts | 18 ++++- src/pages/Search/SearchPageBottomTab.tsx | 68 +++++++++++++++---- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index fe2815e07d5d..09079e598c07 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -32,6 +32,7 @@ import type {SearchDataTypes, 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 SearchStatusBar from './SearchStatusBar'; import type {SearchQueryJSON} from './types'; type HeaderWrapperProps = Pick & { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index db729a9aa77d..08740f66e23f 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,6 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; @@ -38,6 +39,7 @@ type SearchProps = { queryJSON: SearchQueryJSON; isCustomQuery: boolean; policyIDs?: string; + onSearchListScroll?: (event: NativeSyntheticEvent) => void; }; const transactionItemMobileHeight = 100; @@ -74,7 +76,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact return {...selectedTransactions, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}}; } -function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { +function Search({queryJSON, policyIDs, isCustomQuery, onSearchListScroll}: SearchProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -200,6 +202,12 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { queryJSON={queryJSON} hash={hash} /> + {!isSmallScreenWidth && ( + + )} ); @@ -225,10 +233,12 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { queryJSON={queryJSON} hash={hash} /> - + {!isSmallScreenWidth && ( + + )} ); @@ -322,10 +332,12 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { setOfflineModalOpen={() => setOfflineModalVisible(true)} setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} /> - + {!isSmallScreenWidth && ( + + )} sections={[{data: sortedSelectedData, isDisabled: false}]} turnOnSelectionModeOnLongPress @@ -345,6 +357,7 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { /> ) } + onScroll={onSearchListScroll} canSelectMultiple={canSelectMultiple} customListHeaderHeight={searchHeaderHeight} // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, @@ -376,6 +389,7 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { /> ) : undefined } + scrollEventThrottle={16} /> ( shouldUpdateFocusedIndex = false, onLongPressRow, shouldShowListEmptyContent = true, + scrollEventThrottle, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -732,6 +733,7 @@ function BaseSelectionList( ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} + scrollEventThrottle={scrollEventThrottle} /> {children} diff --git a/src/components/SelectionList/index.tsx b/src/components/SelectionList/index.tsx index a6fd636cc215..6f08d2d6eade 100644 --- a/src/components/SelectionList/index.tsx +++ b/src/components/SelectionList/index.tsx @@ -32,7 +32,10 @@ function SelectionList(props: BaseSelectionListProps { + onScroll={(event) => { + if (props.onScroll) { + props.onScroll(event); + } // Only dismiss the keyboard whenever the user scrolls the screen if (!isScreenTouched) { return; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 824f5d82a3ce..217dab9c4b92 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,5 +1,16 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; -import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; +import type { + GestureResponderEvent, + InputModeOptions, + LayoutChangeEvent, + NativeScrollEvent, + NativeSyntheticEvent, + SectionListData, + StyleProp, + TextInput, + TextStyle, + ViewStyle, +} from 'react-native'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; @@ -363,7 +374,7 @@ type BaseSelectionListProps = Partial & { initiallyFocusedOptionKey?: string | null; /** Callback to fire when the list is scrolled */ - onScroll?: () => void; + onScroll?: (event: NativeSyntheticEvent) => void; /** Callback to fire when the list is scrolled and the user begins dragging */ onScrollBeginDrag?: () => void; @@ -492,6 +503,9 @@ type BaseSelectionListProps = Partial & { /** Whether to show the empty list content */ shouldShowListEmptyContent?: boolean; + + /** Scroll event throttle for preventing onScroll callbacks to be fired too often */ + scrollEventThrottle?: number; } & TRightHandSideComponent; type SelectionListHandle = { diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 029407026dc8..29d10ccf60e8 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,10 +1,14 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useRef} from 'react'; +import {View} from 'react-native'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; 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 SearchStatusBar from '@components/Search/SearchStatusBar'; import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -27,6 +31,35 @@ function SearchPageBottomTab() { const {clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + const MAX_BAR_OFFSET = 116; + const HEADER_HEIGHT = 196; + const scrollOffset = useRef(0); + const topBarOffset = useSharedValue(0); + const headerHeight = useSharedValue(HEADER_HEIGHT); + const animatedTopBarStyle = useAnimatedStyle(() => ({ + transform: [{translateY: topBarOffset.value}], + zIndex: -1, + })); + const animatedHeaderStyle = useAnimatedStyle(() => ({ + height: headerHeight.value, + })); + + const handleScroll = (event: NativeSyntheticEvent) => { + const currentOffset = event.nativeEvent.contentOffset.y; + const isScrollingDown = currentOffset > scrollOffset.current; + if (isScrollingDown && event.nativeEvent.contentOffset.y > 20) { + const distanceScrolled = currentOffset - scrollOffset.current; + // eslint-disable-next-line react-compiler/react-compiler + topBarOffset.value = Math.max(-MAX_BAR_OFFSET, topBarOffset.value - distanceScrolled); + headerHeight.value = Math.max(80, headerHeight.value - distanceScrolled); + } else if (!isScrollingDown && event.nativeEvent.contentOffset.y + event.nativeEvent.layoutMeasurement.height < event.nativeEvent.contentSize.height - 10) { + topBarOffset.value = withTiming(0, {duration: 300}); + headerHeight.value = withTiming(HEADER_HEIGHT, {duration: 300}); + } + + scrollOffset.current = currentOffset; + }; + const {queryJSON, policyIDs, isCustomQuery} = useMemo(() => { if (!activeCentralPaneRoute || activeCentralPaneRoute.name !== SCREENS.SEARCH.CENTRAL_PANE) { return {queryJSON: undefined, policyIDs: undefined}; @@ -56,17 +89,27 @@ function SearchPageBottomTab() { shouldShowLink={false} > {!selectionMode?.isEnabled && queryJSON ? ( - <> - - - + + + + + + + {shouldUseNarrowLayout && ( + + )} + + ) : ( )} From 6ea15d8588590e14c5c354895587bd789edbb4c7 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 30 Aug 2024 11:16:32 +0200 Subject: [PATCH 02/33] prettify code --- src/CONST.ts | 2 ++ src/components/Search/SearchPageHeader.tsx | 1 - src/components/Search/SearchStatusBar.tsx | 2 +- src/pages/Search/SearchPageBottomTab.tsx | 34 +++++++++++++--------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index be14ba241404..d94f4e7abd82 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5315,6 +5315,8 @@ const CONST = { REPORT_ID: 'reportID', KEYWORD: 'keyword', }, + TYPE_AND_STATUS_BAR_HEIGHT: 106, + SEARCH_HEADER_HEIGHT: 80, }, REFERRER: { diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 8dd4e1b36244..c28eed688aaa 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -32,7 +32,6 @@ import type {SearchDataTypes, 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 SearchStatusBar from './SearchStatusBar'; import type {SearchQueryJSON} from './types'; type HeaderWrapperProps = Pick & { diff --git a/src/components/Search/SearchStatusBar.tsx b/src/components/Search/SearchStatusBar.tsx index 7c1ffeff1818..99aaa5171719 100644 --- a/src/components/Search/SearchStatusBar.tsx +++ b/src/components/Search/SearchStatusBar.tsx @@ -129,7 +129,7 @@ function SearchStatusBar({type, status}: SearchStatusBarProps) { return ( diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 69167ce3e45d..60c334a197c5 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -13,11 +13,13 @@ import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as SearchUtils from '@libs/SearchUtils'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -26,16 +28,15 @@ import SearchTypeMenu from './SearchTypeMenu'; function SearchPageBottomTab() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowHeight} = useWindowDimensions(); const activeCentralPaneRoute = useActiveCentralPaneRoute(); const styles = useThemeStyles(); const {clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); - const MAX_BAR_OFFSET = 116; - const HEADER_HEIGHT = 196; - const scrollOffset = useRef(0); + const scrollOffset = useSharedValue(0); const topBarOffset = useSharedValue(0); - const headerHeight = useSharedValue(HEADER_HEIGHT); + const headerHeight = useSharedValue(CONST.SEARCH.SEARCH_HEADER_HEIGHT + CONST.SEARCH.TYPE_AND_STATUS_BAR_HEIGHT); const animatedTopBarStyle = useAnimatedStyle(() => ({ transform: [{translateY: topBarOffset.value}], zIndex: -1, @@ -45,19 +46,24 @@ function SearchPageBottomTab() { })); const handleScroll = (event: NativeSyntheticEvent) => { - const currentOffset = event.nativeEvent.contentOffset.y; - const isScrollingDown = currentOffset > scrollOffset.current; - if (isScrollingDown && event.nativeEvent.contentOffset.y > 20) { - const distanceScrolled = currentOffset - scrollOffset.current; + const {contentOffset, layoutMeasurement, contentSize} = event.nativeEvent; + if (windowHeight + CONST.SEARCH.TYPE_AND_STATUS_BAR_HEIGHT > contentSize.height) { + return; + } + + const currentOffset = contentOffset.y; + const isScrollingDown = currentOffset > scrollOffset.value; + if (isScrollingDown && contentOffset.y > 20) { + const distanceScrolled = currentOffset - scrollOffset.value; // eslint-disable-next-line react-compiler/react-compiler - topBarOffset.value = Math.max(-MAX_BAR_OFFSET, topBarOffset.value - distanceScrolled); - headerHeight.value = Math.max(80, headerHeight.value - distanceScrolled); - } else if (!isScrollingDown && event.nativeEvent.contentOffset.y + event.nativeEvent.layoutMeasurement.height < event.nativeEvent.contentSize.height - 10) { + topBarOffset.value = Math.max(-CONST.SEARCH.TYPE_AND_STATUS_BAR_HEIGHT, topBarOffset.value - distanceScrolled); + headerHeight.value = Math.max(CONST.SEARCH.SEARCH_HEADER_HEIGHT, headerHeight.value - distanceScrolled); + } else if (!isScrollingDown && contentOffset.y + layoutMeasurement.height < contentSize.height - 10) { topBarOffset.value = withTiming(0, {duration: 300}); - headerHeight.value = withTiming(HEADER_HEIGHT, {duration: 300}); + headerHeight.value = withTiming(CONST.SEARCH.SEARCH_HEADER_HEIGHT + CONST.SEARCH.TYPE_AND_STATUS_BAR_HEIGHT, {duration: 300}); } - scrollOffset.current = currentOffset; + scrollOffset.value = currentOffset; }; const {queryJSON, policyID, isCustomQuery} = useMemo(() => { From ec1402b11434c61cf8ab22e5d76c21c009b6e896 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 2 Sep 2024 10:46:17 +0200 Subject: [PATCH 03/33] adjust padding --- src/components/Search/index.tsx | 1 + src/components/SelectionList/BaseSelectionList.tsx | 2 ++ src/components/SelectionList/types.ts | 3 +++ 3 files changed, 6 insertions(+) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 900edb0eadac..09667663a09b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -389,6 +389,7 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { ) : undefined } scrollEventThrottle={16} + contentContainerStyle={styles.mt3} /> ( onLongPressRow, shouldShowListEmptyContent = true, scrollEventThrottle, + contentContainerStyle, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -734,6 +735,7 @@ function BaseSelectionList( onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} scrollEventThrottle={scrollEventThrottle} + contentContainerStyle={contentContainerStyle} /> {children} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 217dab9c4b92..41c1de7ec8d4 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -506,6 +506,9 @@ type BaseSelectionListProps = Partial & { /** Scroll event throttle for preventing onScroll callbacks to be fired too often */ scrollEventThrottle?: number; + + /** Additional styles to apply to scrollable content */ + contentContainerStyle?: StyleProp; } & TRightHandSideComponent; type SelectionListHandle = { From e93ceca6007209656f5cc70f456f26d4d2e53156 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 2 Sep 2024 18:11:47 +0200 Subject: [PATCH 04/33] make SearchPageHeader use SearchContext instead of accepting data param --- src/components/Search/SearchContext.tsx | 22 +++++- src/components/Search/SearchPageHeader.tsx | 20 +---- src/components/Search/SearchStatusBar.tsx | 8 ++ src/components/Search/index.tsx | 87 ++++++++-------------- src/components/Search/types.ts | 6 +- src/libs/SearchUtils.ts | 14 +++- src/pages/Search/SearchPage.tsx | 19 ++++- 7 files changed, 95 insertions(+), 81 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 3408ffbc4803..fbfdb4207d9f 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,21 +1,27 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; +import type {SearchReport} from '@src/types/onyx/SearchResults'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {SearchContext, SelectedTransactions} from './types'; const defaultSearchContext = { currentSearchHash: -1, selectedTransactions: {}, + selectedReports: [], setCurrentSearchHash: () => {}, setSelectedTransactions: () => {}, clearSelectedTransactions: () => {}, + setSelectedReports: () => {}, + shouldShowStatusBarLoading: false, + setShouldShowStatusBarLoading: () => {}, }; const Context = React.createContext(defaultSearchContext); 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) => { @@ -45,14 +51,26 @@ function SearchContextProvider({children}: ChildrenProps) { [searchContextData.currentSearchHash], ); + const setSelectedReports = useCallback((selectedReports: Array) => { + setSearchContextData((prevState) => ({ + ...prevState, + selectedReports, + })); + }, []); + + const [shouldShowStatusBarLoading, setShouldShowStatusBarLoading] = useState(false); + const searchContext = useMemo( () => ({ ...searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions, + setSelectedReports, + shouldShowStatusBarLoading, + setShouldShowStatusBarLoading, }), - [searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions], + [searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions, setSelectedReports, shouldShowStatusBarLoading], ); return {children}; diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index c28eed688aaa..091dcf13f2fd 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -10,7 +10,6 @@ import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/typ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; -import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import Text from '@components/Text'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; @@ -28,7 +27,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'; @@ -96,7 +95,6 @@ type SearchPageHeaderProps = { isCustomQuery: boolean; setOfflineModalOpen?: () => void; setDownloadErrorModalOpen?: () => void; - data?: TransactionListItemType[] | ReportListItemType[]; }; type SearchHeaderOptionValue = DeepValueOf | undefined; @@ -118,30 +116,18 @@ function getHeaderContent(type: SearchDataTypes): HeaderContent { } } -function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, isCustomQuery, data}: SearchPageHeaderProps) { +function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, isCustomQuery}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); const {isSmallScreenWidth} = useResponsiveLayout(); - const {selectedTransactions, clearSelectedTransactions} = useSearchContext(); + const {selectedTransactions, clearSelectedTransactions, selectedReports} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); 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 {status, type} = queryJSON; const headerSubtitle = isCustomQuery ? SearchUtils.getSearchHeaderTitle(queryJSON) : translate(getHeaderContent(type).titleText); const headerTitle = isCustomQuery ? translate('search.filtersHeader') : ''; diff --git a/src/components/Search/SearchStatusBar.tsx b/src/components/Search/SearchStatusBar.tsx index 99aaa5171719..8c56fa521756 100644 --- a/src/components/Search/SearchStatusBar.tsx +++ b/src/components/Search/SearchStatusBar.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; +import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -13,6 +14,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type IconAsset from '@src/types/utils/IconAsset'; +import {useSearchContext} from './SearchContext'; import type {ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types'; type SearchStatusBarProps = { @@ -126,6 +128,12 @@ function SearchStatusBar({type, status}: SearchStatusBarProps) { const theme = useTheme(); const {translate} = useLocalize(); const options = getOptions(type); + const {shouldShowStatusBarLoading} = useSearchContext(); + + /** We only want to display the skeleton for the status filters the first time we load them for a specific data type */ + if (shouldShowStatusBarLoading) { + return ; + } return ( >(); const lastSearchResultsRef = useRef>(); - const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions} = useSearchContext(); + const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setSelectedReports, setShouldShowStatusBarLoading} = useSearchContext(); const {selectionMode} = useMobileSelectionMode(); const [offset, setOffset] = useState(0); const [offlineModalVisible, setOfflineModalVisible] = useState(false); + console.log('%%%%%\n', 'i render'); + // DO PRZENIESIENIA const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); + // DO PRZENIESIENIA const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); + // DO PRZENIESIENIA const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const {type, status, sortBy, sortOrder, hash} = queryJSON; @@ -124,11 +126,11 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { SearchActions.search({queryJSON, offset}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOffline, offset, queryJSON]); - + // DO PRZENIESIENIA const handleOnCancelConfirmModal = () => { setDeleteExpensesConfirmModalVisible(false); }; - + // DO PRZENIESIENIA const handleDeleteExpenses = () => { if (selectedTransactionsToDelete.length === 0) { return; @@ -138,7 +140,7 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { setDeleteExpensesConfirmModalVisible(false); SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete); }; - + // DO PRZENIESIENIA const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { setSelectedTransactionsToDelete(itemsToDelete); setDeleteExpensesConfirmModalVisible(true); @@ -188,6 +190,10 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { const isSearchResultsEmpty = !searchResults?.data || SearchUtils.isSearchResultsEmpty(searchResults); const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty); + useEffect(() => { + setShouldShowStatusBarLoading(shouldShowLoadingState && searchResults?.search?.type === type); + }, [searchResults?.search?.type, setShouldShowStatusBarLoading, shouldShowLoadingState, type]); + useEffect(() => { if (!isSearchResultsEmpty || prevIsSearchResultEmpty) { return; @@ -196,26 +202,7 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { }, [isSearchResultsEmpty, prevIsSearchResultEmpty]); if (shouldShowLoadingState) { - return ( - <> - - - {/* We only want to display the skeleton for the status filters the first time we load them for a specific data type */} - {searchResults?.search?.type === type ? ( - - ) : ( - - )} - - - ); + return ; } if (searchResults === undefined) { @@ -231,22 +218,7 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { const shouldShowEmptyState = !isDataLoaded || data.length === 0; if (shouldShowEmptyState) { - return ( - <> - - {!isSmallScreenWidth && ( - - )} - - - ); + return ; } const toggleTransaction = (item: TransactionListItemType | ReportListItemType) => { @@ -255,7 +227,9 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { return; } - setSelectedTransactions(prepareTransactionsList(item, selectedTransactions)); + const transactionList = prepareTransactionsList(item, selectedTransactions); + setSelectedTransactions(transactionList); + setSelectedReports(SearchUtils.getReportsFromSelectedTransactions(data, transactionList)); return; } @@ -267,13 +241,16 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { }); setSelectedTransactions(reducedSelectedTransactions); + setSelectedReports(SearchUtils.getReportsFromSelectedTransactions(data, reducedSelectedTransactions)); return; } - setSelectedTransactions({ + const newSelectedTransactions = { ...selectedTransactions, ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)), - }); + }; + setSelectedTransactions(newSelectedTransactions); + setSelectedReports(SearchUtils.getReportsFromSelectedTransactions(data, newSelectedTransactions)); }; const openReport = (item: TransactionListItemType | ReportListItemType) => { @@ -315,7 +292,9 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { return; } - setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry))); + const allSelectedTransactions = Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry)); + setSelectedTransactions(allSelectedTransactions); + setSelectedReports(SearchUtils.getReportsFromSelectedTransactions(data, allSelectedTransactions)); }; const onSortPress = (column: SearchColumnType, order: SortOrder) => { @@ -332,17 +311,10 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { isCustomQuery={isCustomQuery} queryJSON={queryJSON} hash={hash} - onSelectDeleteOption={handleOnSelectDeleteOption} - data={data} - setOfflineModalOpen={() => setOfflineModalVisible(true)} - setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} + onSelectDeleteOption={handleOnSelectDeleteOption} // NIE POTRZEBNE + setOfflineModalOpen={() => setOfflineModalVisible(true)} // NIE POTRZEBNE + setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} // NIE POTRZEBNE /> - {!isSmallScreenWidth && ( - - )} sections={[{data: sortedSelectedData, isDisabled: false}]} turnOnSelectionModeOnLongPress @@ -397,6 +369,7 @@ function Search({queryJSON, isCustomQuery, onSearchListScroll}: SearchProps) { scrollEventThrottle={16} contentContainerStyle={styles.mt3} /> + {/* DO PRZENIESIENIA */} + {/* DO PRZENIESIENIA */} setOfflineModalVisible(false)} /> + {/* DO PRZENIESIENIA */} ; setCurrentSearchHash: (hash: number) => void; setSelectedTransactions: (selectedTransactions: SelectedTransactions) => void; clearSelectedTransactions: (hash?: number) => void; + setSelectedReports: (selectedReports: Array) => void; + shouldShowStatusBarLoading: boolean; + setShouldShowStatusBarLoading: (shouldShow: boolean) => void; }; type ASTNode = { diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index b30ff55ef5ae..21e9a16495e9 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,5 +1,5 @@ import type {ValueOf} from 'type-fest'; -import type {ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SearchStatus, SortOrder} from '@components/Search/types'; +import type {ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SearchStatus, SelectedTransactions, SortOrder} from '@components/Search/types'; import ReportListItem from '@components/SelectionList/Search/ReportListItem'; import TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; @@ -577,6 +577,17 @@ function getSearchHeaderTitle(queryJSON: SearchQueryJSON) { return title; } +function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[], selectedTransactions: SelectedTransactions) { + return (data ?? []) + .filter( + (item) => + !isTransactionListItemType(item) && + item.reportID && + item.transactions.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), + ) + .map((item) => item.reportID); +} + function buildCannedSearchQuery(type: SearchDataTypes = CONST.SEARCH.DATA_TYPES.EXPENSE, status: SearchStatus = CONST.SEARCH.STATUS.EXPENSE.ALL): SearchQueryString { return normalizeQuery(`type:${type} status:${status}`); } @@ -601,4 +612,5 @@ export { buildCannedSearchQuery, getExpenseTypeTranslationKey, getChatFiltersTranslationKey, + getReportsFromSelectedTransactions, }; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 37735cddce3f..7214b76ab929 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -40,10 +40,21 @@ function SearchPage({route}: SearchPageProps) { shouldShowLink={false} > {queryJSON && ( - + <> + {/* + */} + + )} From 1e5ff5191b40b755b7799e323b34e88ce9b7ddc6 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 3 Sep 2024 10:15:03 +0200 Subject: [PATCH 05/33] move modals from Search/index to SearchPageHeader --- src/components/Search/SearchPageHeader.tsx | 114 ++++++++++++++------- src/components/Search/index.tsx | 68 +----------- 2 files changed, 78 insertions(+), 104 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 091dcf13f2fd..f8ce06590b43 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,10 +1,12 @@ -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'; @@ -91,10 +93,7 @@ function HeaderWrapper({icon, title, subtitle, children, subtitleStyles = {}}: H type SearchPageHeaderProps = { queryJSON: SearchQueryJSON; hash: number; - onSelectDeleteOption?: (itemsToDelete: string[]) => void; isCustomQuery: boolean; - setOfflineModalOpen?: () => void; - setDownloadErrorModalOpen?: () => void; }; type SearchHeaderOptionValue = DeepValueOf | undefined; @@ -116,7 +115,7 @@ function getHeaderContent(type: SearchDataTypes): HeaderContent { } } -function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, isCustomQuery}: SearchPageHeaderProps) { +function SearchPageHeader({queryJSON, hash, isCustomQuery}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -125,6 +124,9 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa const {isSmallScreenWidth} = useResponsiveLayout(); const {selectedTransactions, clearSelectedTransactions, selectedReports} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); + const [offlineModalVisible, setOfflineModalVisible] = useState(false); + const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); @@ -132,9 +134,18 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa const headerSubtitle = isCustomQuery ? SearchUtils.getSearchHeaderTitle(queryJSON) : translate(getHeaderContent(type).titleText); const headerTitle = isCustomQuery ? translate('search.filtersHeader') : ''; const headerIcon = isCustomQuery ? Illustrations.Filters : getHeaderContent(type).icon; - const subtitleStyles = isCustomQuery ? {} : styles.textHeadlineH2; + const handleDeleteExpenses = () => { + if (selectedTransactionsKeys.length === 0) { + return; + } + + clearSelectedTransactions(); + setDeleteExpensesConfirmModalVisible(false); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); + }; + const headerButtonsOptions = useMemo(() => { if (selectedTransactionsKeys.length === 0) { return []; @@ -149,7 +160,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setOfflineModalVisible(true); return; } @@ -157,7 +168,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa SearchActions.exportSearchItemsToCSV( {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { - setDownloadErrorModalOpen?.(); + setDownloadErrorModalVisible(true); }, ); }, @@ -173,7 +184,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setOfflineModalVisible(true); return; } @@ -195,7 +206,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setOfflineModalVisible(true); return; } @@ -218,11 +229,11 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa shouldCloseModalOnSelect: true, onSelected: () => { if (isOffline) { - setOfflineModalOpen?.(); + setOfflineModalVisible(true); return; } - onSelectDeleteOption?.(selectedTransactionsKeys); + setDeleteExpensesConfirmModalVisible(true); }, }); } @@ -252,15 +263,12 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa selectedTransactionsKeys, selectedTransactions, translate, - onSelectDeleteOption, clearSelectedTransactions, hash, theme.icon, styles.colorMuted, styles.fontWeightNormal, isOffline, - setOfflineModalOpen, - setDownloadErrorModalOpen, activeWorkspaceID, selectedReports, styles.textWrap, @@ -280,30 +288,62 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa } return ( - - {headerButtonsOptions.length > 0 && ( - null} - shouldAlwaysShowDropdownMenu - pressOnEnter - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})} - options={headerButtonsOptions} - isSplitButton={false} + <> + + {headerButtonsOptions.length > 0 && ( + null} + shouldAlwaysShowDropdownMenu + pressOnEnter + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})} + options={headerButtonsOptions} + isSplitButton={false} + /> + )} +