From 633ce2c3237355de28d9e85fa26102da96d4d992 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 4 Jun 2025 23:20:59 +0100 Subject: [PATCH 01/56] Auto hide from/to columns --- .../Search/TransactionListItemRow.tsx | 32 +++++++++++-------- .../SelectionList/SearchTableHeader.tsx | 10 +++--- src/components/SelectionList/types.ts | 4 +++ src/libs/SearchUIUtils.ts | 2 ++ src/types/onyx/SearchResults.ts | 6 ++++ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 16ff60fdb7a2..c7cd76653da1 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -405,20 +405,24 @@ function TransactionListItemRow({ isLargeScreenWidth /> - - - - - - + {item.shouldShowTo && ( + + + + )} + {item.shouldShowFrom && ( + + + + )} {item.shouldShowCategory && ( true, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (data: OnyxTypes.SearchResults['data']) => !getShouldShowMerchant(data), - [CONST.SEARCH.TABLE_COLUMNS.FROM]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.TO]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAG]: (data, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.FROM]: (_, metadata) => metadata?.columnsToShow?.shouldShowFromColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TO]: (_, metadata) => metadata?.columnsToShow?.shouldShowToColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (_, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAG]: (_, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (_, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => true, diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 07a5ba64a923..d509ef384d30 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -246,6 +246,10 @@ type TransactionListItemType = ListItem & /** Whether we should show the tag column */ shouldShowTag: boolean; + shouldShowFrom: boolean; + + shouldShowTo: boolean; + /** Whether we should show the tax column */ shouldShowTax: boolean; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 68149436b367..4949123c568f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -344,6 +344,8 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn, shouldShowTag: metadata?.columnsToShow?.shouldShowTagColumn, shouldShowTax: metadata?.columnsToShow?.shouldShowTaxColumn, + shouldShowFrom: metadata?.columnsToShow?.shouldShowFromColumn, + shouldShowTo: metadata?.columnsToShow?.shouldShowToColumn, keyForList: transactionItem.transactionID, shouldShowYear: doesDataContainAPastYearTransaction, }; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index c0b12697c32a..77cbbaf74802 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -43,6 +43,12 @@ type ColumnsToShow = { /** Whether the tax column should be shown */ shouldShowTaxColumn: boolean; + + /** Whether the tax column should be shown */ + shouldShowFromColumn: boolean; + + /** Whether the tax column should be shown */ + shouldShowToColumn: boolean; }; /** Model of search result state */ From 82bda05abb443b090e0e54f360b432d5ac06c85d Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 5 Jun 2025 20:40:24 +0100 Subject: [PATCH 02/56] Bring back condition --- .../SelectionList/Search/TransactionListItemRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index c7cd76653da1..a88f36ea56f3 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -405,7 +405,7 @@ function TransactionListItemRow({ isLargeScreenWidth /> - {item.shouldShowTo && ( + {item.shouldShowFrom && ( )} - {item.shouldShowFrom && ( + {item.shouldShowTo && ( Date: Thu, 5 Jun 2025 20:40:30 +0100 Subject: [PATCH 03/56] Initial work on animation --- src/components/Search/index.tsx | 150 ++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index bddb75fee6a2..d814495cf157 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,8 +1,9 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -344,6 +345,59 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS } }, [isFocused, data, searchResults?.search?.hasMoreResults, selectedTransactions, setExportMode, setShouldShowExportModeOption, shouldGroupByReports]); + // Add animation values - start visible for proper fade out + const fadeOpacity = useSharedValue(1); + const [isInitialMount, setIsInitialMount] = useState(true); + const [isUnmounting, setIsUnmounting] = useState(false); + + const fadeAnimatedStyle = useAnimatedStyle(() => ({ + opacity: fadeOpacity.get(), + })); + + // Handle initial mount fade in + useEffect(() => { + if (!isInitialMount) { + return; + } + + // Start invisible on mount + fadeOpacity.set(0); + + // Then fade in after interactions + InteractionManager.runAfterInteractions(() => { + fadeOpacity.set( + withTiming(1, { + duration: 200, + easing: Easing.inOut(Easing.ease), + }), + ); + }); + + setIsInitialMount(false); + }, [fadeOpacity, isInitialMount]); + + // Handle fade out when component loses focus (before unmount) + useEffect(() => { + if (!isFocused && !isInitialMount && !isUnmounting) { + setIsUnmounting(true); + fadeOpacity.set( + withTiming(0, { + duration: 150, + easing: Easing.inOut(Easing.ease), + }), + ); + } else if (isFocused && isUnmounting) { + // Reset if component regains focus + setIsUnmounting(false); + fadeOpacity.set( + withTiming(1, { + duration: 200, + easing: Easing.inOut(Easing.ease), + }), + ); + } + }, [isFocused, isInitialMount, isUnmounting, fadeOpacity]); + const toggleTransaction = useCallback( (item: SearchListItem) => { if (isReportActionListItemType(item)) { @@ -510,12 +564,12 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS if (shouldShowEmptyState(isDataLoaded, data.length, searchResults.search.type)) { return ( - + - + ); } @@ -562,50 +616,52 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const shouldShowTableHeader = isLargeScreenWidth && !isChat; return ( - - - ) - } - contentContainerStyle={[contentContainerStyle, styles.pb3]} - containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - shouldGroupByReports={shouldGroupByReports} - onScroll={onSearchListScroll} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - ListFooterComponent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - queryJSONHash={hash} - onViewableItemsChanged={onViewableItemsChanged} - onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} - /> - + + + + ) + } + contentContainerStyle={[contentContainerStyle, styles.pb3]} + containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + shouldGroupByReports={shouldGroupByReports} + onScroll={onSearchListScroll} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + ListFooterComponent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + queryJSONHash={hash} + onViewableItemsChanged={onViewableItemsChanged} + onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} + /> + + ); } From 55d6356513d0109ac99d3cd596c195dee33e79a7 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 5 Jun 2025 21:42:19 +0100 Subject: [PATCH 04/56] Move animation to index page --- src/components/Search/index.tsx | 53 +++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index d814495cf157..8cfb3abc7799 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -348,7 +348,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS // Add animation values - start visible for proper fade out const fadeOpacity = useSharedValue(1); const [isInitialMount, setIsInitialMount] = useState(true); - const [isUnmounting, setIsUnmounting] = useState(false); + const hashRef = useRef(hash); const fadeAnimatedStyle = useAnimatedStyle(() => ({ opacity: fadeOpacity.get(), @@ -374,29 +374,50 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS }); setIsInitialMount(false); - }, [fadeOpacity, isInitialMount]); + hashRef.current = hash; + }, [fadeOpacity, isInitialMount, hash]); - // Handle fade out when component loses focus (before unmount) + // Handle fade out/in when filters change (hash changes) useEffect(() => { - if (!isFocused && !isInitialMount && !isUnmounting) { - setIsUnmounting(true); + // Skip if this is initial mount + if (isInitialMount) { + return; + } + + // Check if hash actually changed + if (hashRef.current !== hash) { + // Hash changed - filters changed, start fade out fadeOpacity.set( withTiming(0, { - duration: 150, - easing: Easing.inOut(Easing.ease), - }), - ); - } else if (isFocused && isUnmounting) { - // Reset if component regains focus - setIsUnmounting(false); - fadeOpacity.set( - withTiming(1, { - duration: 200, + duration: 3000, easing: Easing.inOut(Easing.ease), }), ); + + // Update hash ref + hashRef.current = hash; + } + }, [hash, isInitialMount, fadeOpacity]); + + // Handle fade in when new data is loaded after filter change + useEffect(() => { + // Skip if this is initial mount + if (isInitialMount) { + return; + } + + // If data is loaded and we're not showing loading state, fade in + if (isDataLoaded && !shouldShowLoadingState) { + InteractionManager.runAfterInteractions(() => { + fadeOpacity.set( + withTiming(1, { + duration: 200, + easing: Easing.inOut(Easing.ease), + }), + ); + }); } - }, [isFocused, isInitialMount, isUnmounting, fadeOpacity]); + }, [isDataLoaded, shouldShowLoadingState, isInitialMount, fadeOpacity]); const toggleTransaction = useCallback( (item: SearchListItem) => { From bfdcd7c54e1829245dd26c4fefbc34ed60ec4a2b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Sun, 8 Jun 2025 23:09:26 +0100 Subject: [PATCH 05/56] Don't reload initial transactions if we already have them --- src/components/Search/index.tsx | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 42dc31c3e77f..ba34abb48db2 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -23,6 +23,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import {shallowCompare} from '@libs/ObjectUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; import {buildSearchQueryString} from '@libs/SearchQueryUtils'; @@ -217,14 +218,6 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); - useEffect(() => { - if (isOffline) { - return; - } - - handleSearch({queryJSON, offset}); - }, [handleSearch, isOffline, offset, queryJSON]); - const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({ searchResults, transactions, @@ -250,6 +243,20 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS return getSections(type, status, searchResults.data, searchResults.search, shouldGroupByReports); }, [searchResults, isDataLoaded, type, status, shouldGroupByReports]); + const previousQuery = usePrevious(queryJSON); + useEffect(() => { + if (isOffline) { + return; + } + + // If we already loaded initial transactions and we scroll back to the top again, don't reload them + if (data?.length && shallowCompare(previousQuery, queryJSON) && offset === 0) { + return; + } + + handleSearch({queryJSON, offset}); + }, [handleSearch, isOffline, offset, queryJSON, data?.length, previousQuery]); + useEffect(() => { /** We only want to display the skeleton for the status filters the first time we load them for a specific data type */ setShouldShowFiltersBarLoading(shouldShowLoadingState && lastSearchType !== type); @@ -536,6 +543,14 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS [shouldShowLoadingState], ); + const previousColumns = usePrevious(searchResults?.search.columnsToShow); + const currentColumns = useMemo(() => searchResults?.search.columnsToShow, [searchResults?.search.columnsToShow]); + useEffect(() => { + if (previousColumns && !shallowCompare(previousColumns, currentColumns)) { + // todo trigger animation + } + }, [previousColumns, currentColumns]); + if (shouldShowLoadingState) { return ( Date: Thu, 12 Jun 2025 21:07:26 +0100 Subject: [PATCH 06/56] wip --- Mobile-Expensify | 2 +- src/components/Search/SearchList.tsx | 18 ++++++++++++++++++ src/types/onyx/SearchResults.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 29278adfb7b4..7e0185642203 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 29278adfb7b4a512d1b34cdf93d5dbb6992fe353 +Subproject commit 7e0185642203b4ffd484ed338f53b6624a0dd94b diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index 00d405b8e76f..2c814fb102c7 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -22,15 +22,18 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {isMobileChrome} from '@libs/Browser'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; +import {shallowCompare} from '@libs/ObjectUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ColumnsToShow} from '@src/types/onyx/SearchResults'; type SearchListItem = TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType; type SearchListItemComponentType = typeof TransactionListItem | typeof ChatListItem | typeof ReportListItem | typeof TaskListItem; @@ -46,6 +49,12 @@ type SearchListProps = Pick, 'onScroll' /** Default renderer for every item in the list */ ListItem: SearchListItemComponentType; + /** The columns to show in the list */ + columnsToShow?: ColumnsToShow; + + /** Callback to fire when columns change */ + onColumnsChange?: (columnsToShow: ColumnsToShow) => void; + SearchTableHeader?: React.JSX.Element; /** Callback to fire when a row is pressed */ @@ -87,6 +96,8 @@ const onScrollToIndexFailed = () => {}; function SearchList( { data, + columnsToShow, + onColumnsChange, ListItem, SearchTableHeader, onSelectRow, @@ -138,6 +149,13 @@ function SearchList( canBeMissing: true, }); + const previousColumns = usePrevious(columnsToShow); + useEffect(() => { + if (previousColumns && columnsToShow && !shallowCompare(previousColumns, columnsToShow)) { + onColumnsChange?.(columnsToShow); + } + }, [previousColumns, columnsToShow, onColumnsChange]); + useEffect(() => { selectionRef.current = selectedItemsLength; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 67776f5cbc7e..64bac489fbfb 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -494,4 +494,5 @@ export type { SearchReport, SearchReportAction, SearchPolicy, + ColumnsToShow, }; From 0e83ab934d032cf81379d72bd73320cb4e98174c Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 30 Jun 2025 21:19:51 +0100 Subject: [PATCH 07/56] Add From column logic --- src/libs/SearchUIUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 63a75f00fdc3..e05e34e2b7e2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -504,7 +504,8 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata const shouldShowCategory = metadata?.columnsToShow?.shouldShowCategoryColumn; const shouldShowTag = metadata?.columnsToShow?.shouldShowTagColumn; const shouldShowTax = metadata?.columnsToShow?.shouldShowTaxColumn; - const shouldShowTo = metadata?.columnsToShow?.shouldShowTo; + const shouldShowTo = metadata?.columnsToShow?.shouldShowToColumn; + const shouldShowFrom = metadata?.columnsToShow?.shouldShowFromColumn; // Pre-filter transaction keys to avoid repeated checks const transactionKeys = Object.keys(data).filter(isTransactionEntry); @@ -546,6 +547,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata shouldShowTag, shouldShowTax, shouldShowTo, + shouldShowFrom, keyForList: transactionItem.transactionID, shouldShowYear: doesDataContainAPastYearTransaction, isAmountColumnWide: shouldShowAmountInWideColumn, From e979eb3477702a1252df8b9d526168623d48f2dc Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 30 Jun 2025 22:51:29 +0100 Subject: [PATCH 08/56] Add Description column logic --- src/CONST/index.ts | 1 + .../Search/TransactionListItem.tsx | 10 +++++- .../SelectionList/SearchTableHeader.tsx | 2 +- src/components/SelectionList/types.ts | 3 ++ src/components/TransactionItemRow/index.tsx | 33 ++++++++++++++----- src/libs/SearchUIUtils.ts | 2 ++ src/types/onyx/SearchResults.ts | 3 ++ 7 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 04e6d43d6a4a..8dc37c48b603 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1260,6 +1260,7 @@ const CONST = { RECEIPT: 'receipt', DATE: 'date', MERCHANT: 'merchant', + DESCRIPTION: 'description', FROM: 'from', TO: 'to', CATEGORY: 'category', diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index 2fe8a50a95aa..5c2a7c7528f4 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -72,6 +72,7 @@ function TransactionListItem({ CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE, CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE, CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT, + ...(transactionItem?.shouldShowDescription ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] : []), ...(transactionItem?.shouldShowFrom ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.FROM] : []), ...(transactionItem?.shouldShowTo ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] : []), ...(transactionItem?.shouldShowCategory ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] : []), @@ -80,7 +81,14 @@ function TransactionListItem({ CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT, CONST.REPORT.TRANSACTION_LIST.COLUMNS.ACTION, ] satisfies Array>, - [transactionItem?.shouldShowCategory, transactionItem?.shouldShowTag, transactionItem?.shouldShowTax, transactionItem?.shouldShowTo, transactionItem?.shouldShowFrom], + [ + transactionItem?.shouldShowCategory, + transactionItem?.shouldShowTag, + transactionItem?.shouldShowTax, + transactionItem?.shouldShowTo, + transactionItem?.shouldShowFrom, + transactionItem?.shouldShowDescription, + ], ); return ( true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), - [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (data: OnyxTypes.SearchResults['data']) => !getShouldShowMerchant(data), + [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (data, metadata) => metadata?.columnsToShow?.shouldShowDescriptionColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.FROM]: (data, metadata) => metadata?.columnsToShow?.shouldShowFromColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.TO]: (data, metadata) => metadata?.columnsToShow?.shouldShowToColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 63f63b530274..972e24a547da 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -240,6 +240,9 @@ type TransactionListItemType = ListItem & /** Whether we should show the merchant column */ shouldShowMerchant: boolean; + /** Whether the description column should be shown */ + shouldShowDescription: boolean; + /** Whether we should show the category column */ shouldShowCategory: boolean; diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7bed78327133..de3947736cbe 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -106,19 +106,20 @@ type TransactionItemRowProps = { /** If merchant name is empty or (none), then it falls back to description if screen is narrow */ function getMerchantNameWithFallback(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string, shouldUseNarrowLayout?: boolean | undefined) { const shouldShowMerchant = transactionItem.shouldShowMerchant ?? true; - const description = getDescription(transactionItem); - let merchantOrDescriptionToDisplay = transactionItem?.formattedMerchant ?? getMerchant(transactionItem); - const merchantNameEmpty = !merchantOrDescriptionToDisplay || merchantOrDescriptionToDisplay === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + let description = getDescription(transactionItem); + + const merchant = transactionItem?.formattedMerchant ?? getMerchant(transactionItem); + const merchantNameEmpty = !merchant || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; if (merchantNameEmpty && shouldUseNarrowLayout) { - merchantOrDescriptionToDisplay = Parser.htmlToText(description); + description = Parser.htmlToText(description); } - let merchant = shouldShowMerchant ? merchantOrDescriptionToDisplay : Parser.htmlToText(description); + let merchantToDisplay = shouldShowMerchant && !shouldUseNarrowLayout ? merchant : Parser.htmlToText(description); if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem) && shouldShowMerchant) { - merchant = translate('iou.receiptStatusTitle'); + merchantToDisplay = translate('iou.receiptStatusTitle'); } - const merchantName = StringUtils.getFirstLine(merchant); - return merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? merchantName : ''; + const merchantName = StringUtils.getFirstLine(merchantToDisplay); + return merchantToDisplay !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? merchantName : ''; } function TransactionItemRow({ @@ -175,6 +176,7 @@ function TransactionItemRow({ }, [hovered, isParentHovered, isSelected, styles.activeComponentBG, styles.hoveredComponentBG]); const merchantOrDescriptionName = useMemo(() => getMerchantNameWithFallback(transactionItem, translate, shouldUseNarrowLayout), [shouldUseNarrowLayout, transactionItem, translate]); + const description = getDescription(transactionItem); const missingFieldError = useMemo(() => { const hasFieldErrors = hasMissingSmartscanFields(transactionItem); if (hasFieldErrors) { @@ -295,6 +297,20 @@ function TransactionItemRow({ )} ), + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: ( + + {!!description && ( + + )} + + ), [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO]: ( Date: Mon, 30 Jun 2025 23:36:57 +0100 Subject: [PATCH 09/56] Bug fix --- src/components/TransactionItemRow/index.tsx | 61 ++++++++++----------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index de3947736cbe..91d57d08cf1c 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -47,7 +47,7 @@ import TypeCell from './DataCells/TypeCell'; import TransactionItemRowRBRWithOnyx from './TransactionItemRowRBRWithOnyx'; type ColumnComponents = { - [key in ValueOf]: React.ReactElement; + [key in ValueOf]: React.ReactElement | null; }; type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { @@ -103,23 +103,16 @@ type TransactionItemRowProps = { isInSingleTransactionReport?: boolean; }; -/** If merchant name is empty or (none), then it falls back to description if screen is narrow */ -function getMerchantNameWithFallback(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string, shouldUseNarrowLayout?: boolean | undefined) { +function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string) { const shouldShowMerchant = transactionItem.shouldShowMerchant ?? true; - let description = getDescription(transactionItem); - const merchant = transactionItem?.formattedMerchant ?? getMerchant(transactionItem); - const merchantNameEmpty = !merchant || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - if (merchantNameEmpty && shouldUseNarrowLayout) { - description = Parser.htmlToText(description); - } + let merchant = transactionItem?.formattedMerchant ?? getMerchant(transactionItem); - let merchantToDisplay = shouldShowMerchant && !shouldUseNarrowLayout ? merchant : Parser.htmlToText(description); if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem) && shouldShowMerchant) { - merchantToDisplay = translate('iou.receiptStatusTitle'); + merchant = translate('iou.receiptStatusTitle'); } - const merchantName = StringUtils.getFirstLine(merchantToDisplay); - return merchantToDisplay !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? merchantName : ''; + const merchantName = StringUtils.getFirstLine(merchant); + return merchantName !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? merchantName : ''; } function TransactionItemRow({ @@ -175,8 +168,12 @@ function TransactionItemRow({ } }, [hovered, isParentHovered, isSelected, styles.activeComponentBG, styles.hoveredComponentBG]); - const merchantOrDescriptionName = useMemo(() => getMerchantNameWithFallback(transactionItem, translate, shouldUseNarrowLayout), [shouldUseNarrowLayout, transactionItem, translate]); + const merchant = useMemo(() => getMerchantName(transactionItem, translate), [transactionItem, translate]); const description = getDescription(transactionItem); + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const merchantOrDescription = merchant || description; + const missingFieldError = useMemo(() => { const hasFieldErrors = hasMissingSmartscanFields(transactionItem); if (hasFieldErrors) { @@ -283,34 +280,34 @@ function TransactionItemRow({ )} ), - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: ( + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: merchant ? ( - {!!merchantOrDescriptionName && ( + {!!merchant && ( )} - ), - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: ( + ) : null, + // Only show the description column separately if we have both the merchant and the description + // If we're not in narrow layout, merchantOrDescriptionName should alwyas have the merchant and never the description + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: description ? ( - {!!description && ( - - )} + - ), + ) : null, [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO]: ( - {!merchantOrDescriptionName && ( + {!merchantOrDescription && ( )} - {!!merchantOrDescriptionName && ( + {!!merchantOrDescription && ( From d81d2d042d3319ec04b8bf461398fcdfe920b7d6 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 30 Jun 2025 23:39:46 +0100 Subject: [PATCH 10/56] Cleanup --- src/components/TransactionItemRow/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 91d57d08cf1c..c85c33691b60 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -17,7 +17,6 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import Parser from '@libs/Parser'; import StringUtils from '@libs/StringUtils'; import { getDescription, From 08eabed363e93d01d6cac1bf6cfe744068ab171a Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 30 Jun 2025 23:41:06 +0100 Subject: [PATCH 11/56] Remove unneeded comment --- src/components/TransactionItemRow/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index c85c33691b60..af2eb3fd3ec4 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -293,8 +293,6 @@ function TransactionItemRow({ )} ) : null, - // Only show the description column separately if we have both the merchant and the description - // If we're not in narrow layout, merchantOrDescriptionName should alwyas have the merchant and never the description [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: description ? ( Date: Tue, 1 Jul 2025 13:10:49 +0100 Subject: [PATCH 12/56] Fix tests --- .../Search/TransactionGroupListItem.tsx | 1 + src/libs/SearchUIUtils.ts | 1 + tests/unit/Search/SearchUIUtilsTest.ts | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/components/SelectionList/Search/TransactionGroupListItem.tsx b/src/components/SelectionList/Search/TransactionGroupListItem.tsx index 20d4ae5a7c3e..98ca3f8a97fa 100644 --- a/src/components/SelectionList/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionList/Search/TransactionGroupListItem.tsx @@ -104,6 +104,7 @@ function TransactionGroupListItem({ COLUMNS.TYPE, COLUMNS.DATE, COLUMNS.MERCHANT, + ...(sampleTransaction?.shouldShowDescription ? [COLUMNS.DESCRIPTION] : []), ...(sampleTransaction?.shouldShowFrom ? [COLUMNS.FROM] : []), ...(sampleTransaction?.shouldShowTo ? [COLUMNS.TO] : []), ...(sampleTransaction?.shouldShowCategory ? [COLUMNS.CATEGORY] : []), diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 0c9b4c97dcea..8806f3889e8a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -958,6 +958,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx formattedMerchant, date, shouldShowMerchant, + shouldShowDescription: metadata?.columnsToShow.shouldShowDescriptionColumn, shouldShowFrom: metadata?.columnsToShow.shouldShowFromColumn, shouldShowTo: metadata?.columnsToShow.shouldShowToColumn, shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn, diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index c72d0eb1f88c..16874a35d303 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -398,6 +398,9 @@ const searchResults: OnyxTypes.SearchResults = { shouldShowCategoryColumn: true, shouldShowTagColumn: false, shouldShowTaxColumn: false, + shouldShowFromColumn: true, + shouldShowToColumn: true, + shouldShowDescriptionColumn: false, }, hasMoreResults: false, hasResults: true, @@ -479,10 +482,13 @@ const transactionsListItems = [ reportID: '123456789', reportType: 'expense', shouldShowCategory: true, + shouldShowDescription: false, shouldShowMerchant: true, shouldShowTag: false, shouldShowTax: false, shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -546,6 +552,9 @@ const transactionsListItems = [ shouldShowTag: false, shouldShowTax: false, shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, + shouldShowDescription: false, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -625,6 +634,9 @@ const transactionsListItems = [ shouldShowTax: false, keyForList: '3', shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, + shouldShowDescription: false, isAmountColumnWide: false, isTaxAmountColumnWide: false, receipt: undefined, @@ -689,6 +701,9 @@ const transactionsListItems = [ shouldShowTax: false, keyForList: '3', shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, + shouldShowDescription: false, isAmountColumnWide: false, isTaxAmountColumnWide: false, receipt: undefined, @@ -782,6 +797,9 @@ const transactionReportGroupListItems = [ shouldShowTag: false, shouldShowTax: false, shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, + shouldShowDescription: false, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -888,6 +906,9 @@ const transactionReportGroupListItems = [ shouldShowTag: false, shouldShowTax: false, shouldShowYear: true, + shouldShowFrom: true, + shouldShowTo: true, + shouldShowDescription: false, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -1490,6 +1511,9 @@ describe('SearchUIUtils', () => { shouldShowCategoryColumn: true, shouldShowTagColumn: true, shouldShowTaxColumn: true, + shouldShowFromColumn: true, + shouldShowToColumn: true, + shouldShowDescriptionColumn: false, }, }, }; From a11aeac4ad9138b3b5b08ff38c55a53d877c051f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 1 Jul 2025 14:29:45 +0100 Subject: [PATCH 13/56] Bug fix --- src/components/TransactionItemRow/index.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 8a15c6f54f1d..8f6273748e42 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -74,6 +74,9 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { /** information about whether to show merchant, that is provided on Reports page */ shouldShowMerchant?: boolean; + /** information about whether to show the description, that is provided on Reports page */ + shouldShowDescription?: boolean; + /** Type of transaction */ transactionType?: ValueOf; @@ -280,7 +283,7 @@ function TransactionItemRow({ )} ), - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: merchant ? ( + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: transactionItem.shouldShowMerchant ? ( ) : null, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: description ? ( + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: transactionItem.shouldShowDescription ? ( - + {!!description && ( + + )} ) : null, [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO]: ( From 2abbd156e129f09ca469eacee4c48f98cc040404 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 1 Jul 2025 15:14:26 +0100 Subject: [PATCH 14/56] Add animation --- src/components/Search/index.tsx | 104 ++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ad47dbc87929..7e71a08f9b02 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withSequence, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -16,11 +17,12 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import {openSearch, updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search'; +import {openSearch, search, updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import {shallowCompare} from '@libs/ObjectUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; import {buildSearchQueryString} from '@libs/SearchQueryUtils'; @@ -245,6 +247,15 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const shouldShowLoadingState = !isOffline && (!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0)); const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty); + const previousColumns = usePrevious(searchResults?.search.columnsToShow); + + const shouldAnimate = useMemo(() => { + if (!shallowCompare(previousColumns, searchResults?.search.columnsToShow)) { + return true; + } + + return !shouldShowLoadingMoreItems; + }, [shouldShowLoadingMoreItems, previousColumns, searchResults?.search.columnsToShow]); const data = useMemo(() => { if (searchResults === undefined || !isDataLoaded) { @@ -588,49 +599,54 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS return ( - - ) - } - contentContainerStyle={[contentContainerStyle, styles.pb3]} - containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - onScroll={onSearchListScroll} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - ListFooterComponent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - queryJSON={queryJSON} - onViewableItemsChanged={onViewableItemsChanged} - onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} - /> + + + ) + } + contentContainerStyle={[contentContainerStyle, styles.pb3]} + containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + onScroll={onSearchListScroll} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + ListFooterComponent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + queryJSON={queryJSON} + onViewableItemsChanged={onViewableItemsChanged} + onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} + /> + ); } From e3198e56c36ae3b465cf44caeb02cf2dd842a07b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 1 Jul 2025 15:55:26 +0100 Subject: [PATCH 15/56] Lint --- src/components/Search/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 7e71a08f9b02..45472215bbf2 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withSequence, withTiming} from 'react-native-reanimated'; +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -17,7 +17,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import {openSearch, search, updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search'; +import {openSearch, updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; From a7671af8a661b5629a550d24290ba3462e2a53e0 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 2 Jul 2025 19:51:45 +0100 Subject: [PATCH 16/56] Remove animation --- src/components/Search/index.tsx | 102 ++++++++++++++------------------ 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 45472215bbf2..ad47dbc87929 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -22,7 +21,6 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {shallowCompare} from '@libs/ObjectUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; import {buildSearchQueryString} from '@libs/SearchQueryUtils'; @@ -247,15 +245,6 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const shouldShowLoadingState = !isOffline && (!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0)); const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty); - const previousColumns = usePrevious(searchResults?.search.columnsToShow); - - const shouldAnimate = useMemo(() => { - if (!shallowCompare(previousColumns, searchResults?.search.columnsToShow)) { - return true; - } - - return !shouldShowLoadingMoreItems; - }, [shouldShowLoadingMoreItems, previousColumns, searchResults?.search.columnsToShow]); const data = useMemo(() => { if (searchResults === undefined || !isDataLoaded) { @@ -599,54 +588,49 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS return ( - - - ) - } - contentContainerStyle={[contentContainerStyle, styles.pb3]} - containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - onScroll={onSearchListScroll} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - ListFooterComponent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - queryJSON={queryJSON} - onViewableItemsChanged={onViewableItemsChanged} - onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} - /> - + + ) + } + contentContainerStyle={[contentContainerStyle, styles.pb3]} + containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + onScroll={onSearchListScroll} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + ListFooterComponent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + queryJSON={queryJSON} + onViewableItemsChanged={onViewableItemsChanged} + onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} + /> ); } From a4a63140a352ae50687fd3d9cea1127f94486013 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 2 Jul 2025 20:23:14 +0100 Subject: [PATCH 17/56] Lint --- src/components/SelectionList/SearchTableHeader.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index bb47345c2b70..4ecd79830b1e 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -22,12 +22,12 @@ const shouldShowColumnConfig: Record true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), - [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (data, metadata) => metadata?.columnsToShow?.shouldShowDescriptionColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.FROM]: (data, metadata) => metadata?.columnsToShow?.shouldShowFromColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TO]: (data, metadata) => metadata?.columnsToShow?.shouldShowToColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAG]: (data, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (_, metadata) => metadata?.columnsToShow?.shouldShowDescriptionColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.FROM]: (_, metadata) => metadata?.columnsToShow?.shouldShowFromColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TO]: (_, metadata) => metadata?.columnsToShow?.shouldShowToColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (_, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAG]: (_, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (_, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => true, From 820dcb9ca76e75c24ce247cebe9c07d8a4e6798f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 2 Jul 2025 20:23:22 +0100 Subject: [PATCH 18/56] Apply logic to the report view --- .../MoneyRequestReportTableHeader.tsx | 44 ++++++-------- .../MoneyRequestReportTransactionList.tsx | 57 +++++++++++++++++-- src/components/TransactionItemRow/index.tsx | 8 +-- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index e588dde8b829..69d5905b9160 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -13,25 +13,6 @@ type ColumnConfig = { isColumnSortable?: boolean; }; -const shouldShowColumnConfig: Record boolean> = { - [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.TYPE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (isIOUReport) => !isIOUReport, - [CONST.SEARCH.TABLE_COLUMNS.TAG]: (isIOUReport) => !isIOUReport, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.IN]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.FROM]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.TO]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => false, - [CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: () => false, -}; - const columnConfig: ColumnConfig[] = [ { columnName: CONST.SEARCH.TABLE_COLUMNS.RECEIPT, @@ -51,6 +32,10 @@ const columnConfig: ColumnConfig[] = [ columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT, translationKey: 'common.merchant', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, + translationKey: 'common.description', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY, translationKey: 'common.category', @@ -81,19 +66,26 @@ type SearchTableHeaderProps = { isIOUReport: boolean; }; -function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, dateColumnSize, shouldShowSorting, isIOUReport, amountColumnSize, taxAmountColumnSize}: SearchTableHeaderProps) { +function MoneyRequestReportTableHeader({ + sortBy, + sortOrder, + onSortPress, + dateColumnSize, + shouldShowSorting, + isIOUReport, + amountColumnSize, + taxAmountColumnSize, + columnsToShow, +}: SearchTableHeaderProps) { const styles = useThemeStyles(); const shouldShowColumn = useCallback( (columnName: SortableColumnName) => { - const shouldShowFun = shouldShowColumnConfig[columnName]; - if (!shouldShowFun) { - return false; - } - return shouldShowFun(isIOUReport); + return columnsToShow.includes(columnName); }, - [isIOUReport], + [columnsToShow], ); + return ( >; + type SortableColumnName = TupleToUnion; type SortedTransactions = { @@ -186,6 +189,47 @@ function MoneyRequestReportTransactionList({ })); }, [newTransactions, sortBy, sortOrder, transactions]); + const columnsToShow = useMemo(() => { + const columns: Record = { + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT]: true, + }; + + transactions.forEach((transactionItem) => { + const merchant = transactionItem.modifiedMerchant ? transactionItem.modifiedMerchant : (transactionItem.merchant ?? ''); + if (merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT] = true; + } + + console.log('description', getDescription(transactionItem)); + + if (getDescription(transactionItem) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] = true; + } + + if (getCategory(transactionItem) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] = true; + } + + if (getTag(transactionItem) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG] = true; + } + }); + + return Object.keys(columns).filter((columnName: string) => { + return columns[columnName]; + }) as ReportColumnType; + }, [transactions]); + + console.log('columns to show', columnsToShow); + const navigateToTransaction = useCallback( (activeTransaction: OnyxTypes.Transaction) => { const iouAction = getIOUActionForTransactionID(reportActions, activeTransaction.transactionID); @@ -258,6 +302,7 @@ function MoneyRequestReportTransactionList({ setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); }} isIOUReport={isIOUReport(report)} + columnsToShow={columnsToShow} /> )} @@ -314,7 +359,7 @@ function MoneyRequestReportTransactionList({ shouldUseNarrowLayout={shouldUseNarrowLayout || isMediumScreenWidth} shouldShowCheckbox={!!selectionMode?.isEnabled || !isSmallScreenWidth} onCheckboxPress={toggleTransaction} - columns={allReportColumns} + columns={columnsToShow} scrollToNewTransaction={transaction.transactionID === newTransactions?.at(0)?.transactionID ? scrollToNewTransaction : undefined} isInReportTableView /> diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 8f6273748e42..91c2200b1188 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -283,7 +283,7 @@ function TransactionItemRow({ )} ), - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: transactionItem.shouldShowMerchant ? ( + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: ( )} - ) : null, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: transactionItem.shouldShowDescription ? ( + ), + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: ( )} - ) : null, + ), [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO]: ( Date: Wed, 2 Jul 2025 20:24:40 +0100 Subject: [PATCH 19/56] Lint --- .../MoneyRequestReportTableHeader.tsx | 14 ++------- .../MoneyRequestReportTransactionList.tsx | 31 ++++--------------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index 69d5905b9160..1a3e934f0b61 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -63,20 +63,10 @@ type SearchTableHeaderProps = { amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; shouldShowSorting: boolean; - isIOUReport: boolean; + columnsToShow: SortableColumnName[]; }; -function MoneyRequestReportTableHeader({ - sortBy, - sortOrder, - onSortPress, - dateColumnSize, - shouldShowSorting, - isIOUReport, - amountColumnSize, - taxAmountColumnSize, - columnsToShow, -}: SearchTableHeaderProps) { +function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, dateColumnSize, shouldShowSorting, amountColumnSize, taxAmountColumnSize, columnsToShow}: SearchTableHeaderProps) { const styles = useThemeStyles(); const shouldShowColumn = useCallback( diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 075965cc0bce..3e857c2a6a32 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -3,7 +3,7 @@ import isEmpty from 'lodash/isEmpty'; import React, {memo, useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; -import type {TupleToUnion, ValueOf} from 'type-fest'; +import type {TupleToUnion} from 'type-fest'; import {getButtonRole} from '@components/Button/utils'; import Checkbox from '@components/Checkbox'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -11,9 +11,9 @@ import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import {useSearchContext} from '@components/Search/SearchContext'; -import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import type {SortOrder} from '@components/Search/types'; import Text from '@components/Text'; -import TransactionItemRow, {TransactionWithOptionalSearchFields} from '@components/TransactionItemRow'; +import TransactionItemRow from '@components/TransactionItemRow'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useHover from '@hooks/useHover'; import useLocalize from '@hooks/useLocalize'; @@ -30,8 +30,8 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; -import {getMoneyRequestSpendBreakdown, isIOUReport} from '@libs/ReportUtils'; -import {compareValues, getShouldShowMerchant, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; +import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils'; +import {compareValues, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; import {getCategory, getDescription, getTag, getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils'; import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; import Navigation from '@navigation/Navigation'; @@ -78,20 +78,6 @@ const sortableColumnNames = [ CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, ]; -const allReportColumns = [ - CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT, -]; - -type ReportColumnType = Array>; - type SortableColumnName = TupleToUnion; type SortedTransactions = { @@ -208,8 +194,6 @@ function MoneyRequestReportTransactionList({ columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT] = true; } - console.log('description', getDescription(transactionItem)); - if (getDescription(transactionItem) !== '') { columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] = true; } @@ -225,11 +209,9 @@ function MoneyRequestReportTransactionList({ return Object.keys(columns).filter((columnName: string) => { return columns[columnName]; - }) as ReportColumnType; + }) as SortableColumnName[]; }, [transactions]); - console.log('columns to show', columnsToShow); - const navigateToTransaction = useCallback( (activeTransaction: OnyxTypes.Transaction) => { const iouAction = getIOUActionForTransactionID(reportActions, activeTransaction.transactionID); @@ -301,7 +283,6 @@ function MoneyRequestReportTransactionList({ setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); }} - isIOUReport={isIOUReport(report)} columnsToShow={columnsToShow} /> )} From d030335772ad5dc7bef68c161c05d81b00a96fd6 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 2 Jul 2025 21:10:35 +0100 Subject: [PATCH 20/56] Lint --- tests/unit/Search/handleActionButtonPressTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index bdd52532dd5b..55c80bcad1c5 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -183,6 +183,9 @@ const mockReportItemWithHold = { shouldShowCategory: true, shouldShowTag: false, shouldShowTax: false, + shouldShowTo: true, + shouldShowFrom: true, + shouldShowDescription: false, keyForList: '5345995386715609966', shouldShowYear: false, isAmountColumnWide: false, From e54cd51a08ef7162d808a1bd94712cdb65132f94 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 3 Jul 2025 02:05:50 +0100 Subject: [PATCH 21/56] Show "Scanning" merchant if transaction is being scanned --- .../MoneyRequestReportTransactionList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 3e857c2a6a32..11aa337dfb71 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -32,7 +32,7 @@ import {navigationRef} from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils'; import {compareValues, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; -import {getCategory, getDescription, getTag, getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils'; +import {getCategory, getDescription, getTag, getTransactionPendingAction, isScanning, isTransactionPendingDelete} from '@libs/TransactionUtils'; import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; @@ -190,7 +190,7 @@ function MoneyRequestReportTransactionList({ transactions.forEach((transactionItem) => { const merchant = transactionItem.modifiedMerchant ? transactionItem.modifiedMerchant : (transactionItem.merchant ?? ''); - if (merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { + if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) || isScanning(transactionItem)) { columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT] = true; } From f0291cd15aeec9a54398a4d92f28d459e87de71b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 3 Jul 2025 21:02:10 +0100 Subject: [PATCH 22/56] Rename param and clean up code --- .../MoneyRequestReportTableHeader.tsx | 8 ++++---- .../MoneyRequestReportTransactionList.tsx | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index 1a3e934f0b61..c83f12cb4ac5 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -63,17 +63,17 @@ type SearchTableHeaderProps = { amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; shouldShowSorting: boolean; - columnsToShow: SortableColumnName[]; + columns: SortableColumnName[]; }; -function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, dateColumnSize, shouldShowSorting, amountColumnSize, taxAmountColumnSize, columnsToShow}: SearchTableHeaderProps) { +function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, dateColumnSize, shouldShowSorting, amountColumnSize, taxAmountColumnSize, columns}: SearchTableHeaderProps) { const styles = useThemeStyles(); const shouldShowColumn = useCallback( (columnName: SortableColumnName) => { - return columnsToShow.includes(columnName); + return columns.includes(columnName); }, - [columnsToShow], + [columns], ); return ( diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 11aa337dfb71..ebe54cd4b592 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -207,9 +207,7 @@ function MoneyRequestReportTransactionList({ } }); - return Object.keys(columns).filter((columnName: string) => { - return columns[columnName]; - }) as SortableColumnName[]; + return Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; }, [transactions]); const navigateToTransaction = useCallback( @@ -283,7 +281,7 @@ function MoneyRequestReportTransactionList({ setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); }} - columnsToShow={columnsToShow} + columns={columnsToShow} /> )} From f0541e29bb06db824f5612e7c8b924d5669a3c68 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 7 Jul 2025 19:26:42 +0100 Subject: [PATCH 23/56] Move logic of showing columns to the client --- .../MoneyRequestReportTransactionList.tsx | 37 +------- src/components/Search/index.tsx | 10 +- .../SelectionList/SearchTableHeader.tsx | 32 +------ src/libs/SearchUIUtils.ts | 92 +++++++++++++++++++ 4 files changed, 108 insertions(+), 63 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index ebe54cd4b592..f7888f96ee6b 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -31,8 +31,8 @@ import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils'; -import {compareValues, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; -import {getCategory, getDescription, getTag, getTransactionPendingAction, isScanning, isTransactionPendingDelete} from '@libs/TransactionUtils'; +import {compareValues, getColumnsToShow, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; +import {getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils'; import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; @@ -176,38 +176,7 @@ function MoneyRequestReportTransactionList({ }, [newTransactions, sortBy, sortOrder, transactions]); const columnsToShow = useMemo(() => { - const columns: Record = { - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT]: true, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE]: true, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE]: true, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: false, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: false, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY]: false, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG]: false, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: true, - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT]: true, - }; - - transactions.forEach((transactionItem) => { - const merchant = transactionItem.modifiedMerchant ? transactionItem.modifiedMerchant : (transactionItem.merchant ?? ''); - if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) || isScanning(transactionItem)) { - columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT] = true; - } - - if (getDescription(transactionItem) !== '') { - columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] = true; - } - - if (getCategory(transactionItem) !== '') { - columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] = true; - } - - if (getTag(transactionItem) !== '') { - columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG] = true; - } - }); - - return Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; + return getColumnsToShow(transactions, true) as SortableColumnName[]; }, [transactions]); const navigateToTransaction = useCallback( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 6e1f03321b0e..a68bffe16e1e 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -27,6 +27,7 @@ import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; import {buildSearchQueryString} from '@libs/SearchQueryUtils'; import { + getColumnsToShow, getListItem, getSections, getSortedSections, @@ -473,6 +474,13 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS [shouldShowLoadingState], ); + const columnsToShow = useMemo(() => { + if (!searchResults?.data) { + return []; + } + return getColumnsToShow(searchResults?.data); + }, [searchResults?.data]); + if (shouldShowLoadingState) { return ( ) } diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 4ecd79830b1e..7e6509a8a1ef 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -2,41 +2,18 @@ import React, {useCallback} from 'react'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getShouldShowMerchant} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type * as OnyxTypes from '@src/types/onyx'; import SortableTableHeader from './SortableTableHeader'; import type {SortableColumnName} from './types'; -type ShouldShowSearchColumnFn = (data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) => boolean; - type SearchColumnConfig = { columnName: SearchColumnType; translationKey: TranslationPaths; isColumnSortable?: boolean; }; -const shouldShowColumnConfig: Record = { - [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.TYPE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), - [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (_, metadata) => metadata?.columnsToShow?.shouldShowDescriptionColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.FROM]: (_, metadata) => metadata?.columnsToShow?.shouldShowFromColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TO]: (_, metadata) => metadata?.columnsToShow?.shouldShowToColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (_, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAG]: (_, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (_, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, - [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: () => true, - [CONST.SEARCH.TABLE_COLUMNS.IN]: () => true, - // This column is never displayed on Search - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: () => false, -}; - const expenseHeaders: SearchColumnConfig[] = [ { columnName: CONST.SEARCH.TABLE_COLUMNS.RECEIPT, @@ -134,7 +111,6 @@ const SearchColumns = { }; type SearchTableHeaderProps = { - data: OnyxTypes.SearchResults['data']; metadata: OnyxTypes.SearchResults['search']; sortBy?: SearchColumnType; sortOrder?: SortOrder; @@ -144,10 +120,10 @@ type SearchTableHeaderProps = { isTaxAmountColumnWide: boolean; shouldShowSorting: boolean; canSelectMultiple: boolean; + columns: SortableColumnName[]; }; function SearchTableHeader({ - data, metadata, sortBy, sortOrder, @@ -157,6 +133,7 @@ function SearchTableHeader({ canSelectMultiple, isAmountColumnWide, isTaxAmountColumnWide, + columns, }: SearchTableHeaderProps) { const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -165,10 +142,9 @@ function SearchTableHeader({ const shouldShowColumn = useCallback( (columnName: SortableColumnName) => { - const shouldShowFun = shouldShowColumnConfig[columnName]; - return shouldShowFun(data, metadata); + return columns.includes(columnName); }, - [data, metadata], + [columns], ); if (displayNarrowVersion) { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 0f41629d7b56..84a38de477b0 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -16,6 +16,7 @@ import type { ListItem, ReportActionListItemType, SearchListItem, + SortableColumnName, TaskListItemType, TransactionCardGroupListItemType, TransactionGroupListItemType, @@ -79,11 +80,15 @@ import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues} from './Se import StringUtils from './StringUtils'; import {shouldRestrictUserBillableActions} from './SubscriptionUtils'; import { + getCategory, + getDescription, + getTag, getTaxAmount, getAmount as getTransactionAmount, getCreated as getTransactionCreatedDate, getMerchant as getTransactionMerchant, isPendingCardOrScanningTransaction, + isScanning, isViolationDismissed, } from './TransactionUtils'; import shouldShowTransactionYear from './TransactionUtils/shouldShowTransactionYear'; @@ -1080,6 +1085,92 @@ function getSortedSections( return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder); } +/** + * Determines what columns to show based on available data + * @param isExpenseReportView: true when we are inside an expense report view, false if we're in the Reports page. + */ +function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], isExpenseReportView = false) { + const columns: Record = isExpenseReportView + ? { + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG]: false, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: true, + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT]: true, + } + : { + [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: true, + [CONST.SEARCH.TABLE_COLUMNS.TYPE]: true, + [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, + [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, + [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, + [CONST.SEARCH.TABLE_COLUMNS.FROM]: false, + [CONST.SEARCH.TABLE_COLUMNS.TO]: false, + [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, + [CONST.SEARCH.TABLE_COLUMNS.TAG]: false, + [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: false, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: true, + [CONST.SEARCH.TABLE_COLUMNS.ACTION]: true, + [CONST.SEARCH.TABLE_COLUMNS.TITLE]: true, + [CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: true, + [CONST.SEARCH.TABLE_COLUMNS.IN]: true, + // This column is never displayed on Search + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: false, + }; + + const updateColumns = (transaction: OnyxTypes.Transaction | SearchTransaction) => { + const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : (transaction.merchant ?? ''); + if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) || isScanning(transaction)) { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT] = true; + } + + if (getDescription(transaction) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] = true; + } + + if (getCategory(transaction) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] = true; + } + + if (getTag(transaction) !== '') { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG] = true; + } + + if (isExpenseReportView) { + return; + } + + // Handle From&To columns that are only shown in the Reports page + // if From or To differ from current user in any transaction, show the columns + const accountID = (transaction as SearchTransaction).accountID; + if (accountID !== currentAccountID) { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.FROM] = true; + } + + const managerID = (transaction as SearchTransaction).managerID; + if (managerID !== currentAccountID) { + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] = true; + } + }; + + if (Array.isArray(transactions)) { + transactions.forEach(updateColumns); + } else { + Object.keys(transactions).forEach((key) => { + if (!isTransactionEntry(key)) { + return; + } + updateColumns(transactions[key]); + }); + } + + return Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; +} + /** * Compares two values based on a specified sorting order and column. * Handles both string and numeric comparisons, with special handling for absolute values when sorting by total amount. @@ -1720,6 +1811,7 @@ export { isReportActionEntry, isTaskListItemType, getAction, + getColumnsToShow, createTypeMenuSections, createBaseSavedSearchMenuItem, shouldShowEmptyState, From a6d17645aed402655a13b0f0bba19690a58b8fef Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 7 Jul 2025 19:53:57 +0100 Subject: [PATCH 24/56] Fix check of empty values of tag & category --- src/libs/SearchUIUtils.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 84a38de477b0..418fcd312084 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1132,11 +1132,14 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] = true; } - if (getCategory(transaction) !== '') { + const category = getCategory(transaction); + const categoryEmptyValues = CONST.SEARCH.CATEGORY_EMPTY_VALUE.split(','); + if (category !== '' && !categoryEmptyValues.includes(category)) { columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] = true; } - if (getTag(transaction) !== '') { + const tag = getTag(transaction); + if (tag !== '' && tag !== CONST.SEARCH.TAG_EMPTY_VALUE) { columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG] = true; } @@ -1168,7 +1171,9 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy }); } - return Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; + const res = Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; + console.log('columns to show', res); + return res; } /** From 97c84d7b53586365a807dea3bee7b5deac388a4a Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 7 Jul 2025 20:44:30 +0100 Subject: [PATCH 25/56] Fix bug in display of columns --- src/components/Search/SearchList.tsx | 15 +++++++++- src/components/Search/index.tsx | 1 + .../Search/TransactionListItem.tsx | 28 ++----------------- src/components/SelectionList/types.ts | 1 + 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index a6f6bf436092..c341f50a64e9 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -5,6 +5,7 @@ import {View} from 'react-native'; import type {FlatList, ListRenderItemInfo, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; import Animated from 'react-native-reanimated'; import type {FlatListPropsWithLayout} from 'react-native-reanimated'; +import {ValueOf} from 'type-fest'; import Checkbox from '@components/Checkbox'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; @@ -14,7 +15,14 @@ import type ChatListItem from '@components/SelectionList/ChatListItem'; import type TaskListItem from '@components/SelectionList/Search/TaskListItem'; import type TransactionGroupListItem from '@components/SelectionList/Search/TransactionGroupListItem'; import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; -import type {ExtendedTargetedEvent, ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type { + ExtendedTargetedEvent, + ReportActionListItemType, + SortableColumnName, + TaskListItemType, + TransactionGroupListItemType, + TransactionListItemType, +} from '@components/SelectionList/types'; import Text from '@components/Text'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; @@ -78,6 +86,8 @@ type SearchListProps = Pick, 'onScroll' /** Invoked on mount and layout changes */ onLayout?: () => void; + + columns?: SortableColumnName[]; }; const onScrollToIndexFailed = () => {}; @@ -102,6 +112,7 @@ function SearchList( queryJSON, onViewableItemsChanged, onLayout, + columns, }: SearchListProps, ref: ForwardedRef, ) { @@ -335,6 +346,7 @@ function SearchList( shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} queryJSONHash={hash} policies={policies} + columns={columns} isDisabled={isDisabled} allReports={allReports} groupBy={groupBy} @@ -355,6 +367,7 @@ function SearchList( setFocusedIndex, shouldPreventDefaultFocusOnSelectRow, allReports, + columns, ], ); diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a68bffe16e1e..ecc4e2fbd647 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -643,6 +643,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS queryJSON={queryJSON} onViewableItemsChanged={onViewableItemsChanged} onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} + columns={columnsToShow} /> ); diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index 5c2a7c7528f4..d30cf515ed57 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -25,6 +25,7 @@ function TransactionListItem({ onLongPressRow, shouldSyncFocus, isLoading, + columns, }: TransactionListItemProps) { const transactionItem = item as unknown as TransactionListItemType; const styles = useThemeStyles(); @@ -65,31 +66,6 @@ function TransactionListItem({ }; }, [transactionItem]); - const columns = useMemo( - () => - [ - CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT, - ...(transactionItem?.shouldShowDescription ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION] : []), - ...(transactionItem?.shouldShowFrom ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.FROM] : []), - ...(transactionItem?.shouldShowTo ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] : []), - ...(transactionItem?.shouldShowCategory ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY] : []), - ...(transactionItem?.shouldShowTag ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG] : []), - ...(transactionItem?.shouldShowTax ? [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAX] : []), - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.ACTION, - ] satisfies Array>, - [ - transactionItem?.shouldShowCategory, - transactionItem?.shouldShowTag, - transactionItem?.shouldShowTax, - transactionItem?.shouldShowTo, - transactionItem?.shouldShowFrom, - transactionItem?.shouldShowDescription, - ], - ); return ( ({ }} onCheckboxPress={() => onCheckboxPress?.(item)} shouldUseNarrowLayout={!isLargeScreenWidth} - columns={columns} + columns={columns as Array>} isParentHovered={hovered} isActionLoading={isLoading ?? transactionItem.isActionLoading} isSelected={!!transactionItem.isSelected} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 972e24a547da..229ef0d36166 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -457,6 +457,7 @@ type TableListItemProps = ListItemProps; type TransactionListItemProps = ListItemProps & { /** Whether the item's action is loading */ isLoading?: boolean; + columns?: SortableColumnName[]; }; type TaskListItemProps = ListItemProps & { From b72eccbd0490168a926bf739f6c6914f72997847 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 14 Jul 2025 20:21:10 +0100 Subject: [PATCH 26/56] Keep previously shown columns --- .../MoneyRequestReportTransactionList.tsx | 3 +- src/components/Search/index.tsx | 28 +++++++++++++++++-- src/libs/SearchUIUtils.ts | 2 +- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index fdc3c8975369..8d3056e887d5 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -176,7 +176,8 @@ function MoneyRequestReportTransactionList({ }, [newTransactions, sortBy, sortOrder, transactions]); const columnsToShow = useMemo(() => { - return getColumnsToShow(transactions, true) as SortableColumnName[]; + const columns = getColumnsToShow(transactions, true); + return Object.keys(columns).filter((column) => columns[column]) as SortableColumnName[]; }, [transactions]); const navigateToTransaction = useCallback( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f226e89909a6..da32ee35cc12 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,4 +1,5 @@ import {useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; +import {shallowEqual} from 'fast-equals'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native'; import {View} from 'react-native'; @@ -21,6 +22,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import {shallowCompare} from '@libs/ObjectUtils'; import Performance from '@libs/Performance'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; @@ -485,13 +487,35 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [shouldShowLoadingState], ); - const columnsToShow = useMemo(() => { + const currentColumns = useMemo(() => { if (!searchResults?.data) { - return []; + return {}; } return getColumnsToShow(searchResults?.data); }, [searchResults?.data]); + const previousColumnsRef = useRef>({}); + + useEffect(() => { + // Only update if columns actually changed + if (shallowCompare(currentColumns, previousColumnsRef.current)) { + return; + } + previousColumnsRef.current = {...currentColumns}; + }, [currentColumns]); + + const columnsToShow = useMemo(() => { + const columns = {...currentColumns}; + + const previouslyShownColumns = Object.keys(previousColumnsRef.current).filter((col) => previousColumnsRef.current[col] && !columns[col]); + + previouslyShownColumns.forEach((col) => { + columns[col] = true; + }); + + return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; + }, [currentColumns]); + const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || isMobileSelectionModeEnabled); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 7fbd29c2115b..6bdd019079c2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1176,7 +1176,7 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy }); } - return Object.keys(columns).filter((columnName: string) => columns[columnName]) as SortableColumnName[]; + return columns; } /** From 65db2e5be382d94a16dc7092a5b19ad72df0c183 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 14 Jul 2025 20:22:19 +0100 Subject: [PATCH 27/56] Early return if no previous columns --- src/components/Search/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index da32ee35cc12..1763de720b45 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -507,6 +507,10 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const columnsToShow = useMemo(() => { const columns = {...currentColumns}; + if (!previousColumnsRef.current) { + return columns; + } + const previouslyShownColumns = Object.keys(previousColumnsRef.current).filter((col) => previousColumnsRef.current[col] && !columns[col]); previouslyShownColumns.forEach((col) => { From 39e3614aa2bfc9fd3766863b37bcefc984387be2 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 14 Jul 2025 20:44:44 +0100 Subject: [PATCH 28/56] Fixes --- package-lock.json | 8 +- package.json | 2 +- src/components/Search/SearchList.tsx | 71 +++++++++++------- src/components/Search/index.tsx | 108 +++++++++++++-------------- src/pages/Search/SearchPage.tsx | 3 +- 5 files changed, 104 insertions(+), 88 deletions(-) diff --git a/package-lock.json b/package-lock.json index 829540eb6a1b..b334d61b174a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "@react-navigation/stack": "7.3.3", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.33", - "@shopify/flash-list": "1.7.6", + "@shopify/flash-list": "1.8.2", "@ua/react-native-airship": "~24.4.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", @@ -11944,9 +11944,9 @@ } }, "node_modules/@shopify/flash-list": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.7.6.tgz", - "integrity": "sha512-0kuuAbWgy4YSlN05mt0ScvxK8uiDixMsICWvDed+LTxvZ5+5iRyt3M8cRLUroB8sfiZlJJZWlxHrx0frBpsYOQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.8.2.tgz", + "integrity": "sha512-CJRkaMkwbDJtOyO9RgjXIXDKgIpgXPt2fncK7zcNvTPbvZHWnXcnS7Avl+TzAtvpPThV+fl3oxjnOYpszMil2A==", "license": "MIT", "dependencies": { "recyclerlistview": "4.2.3", diff --git a/package.json b/package.json index a9df190d37ba..abd900ce1ccf 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "@react-navigation/stack": "7.3.3", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.33", - "@shopify/flash-list": "1.7.6", + "@shopify/flash-list": "1.8.2", "@ua/react-native-airship": "~24.4.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index 8e99d584f337..541cce6ef18a 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -6,8 +6,6 @@ import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import Animated from 'react-native-reanimated'; -import type {FlatListPropsWithLayout} from 'react-native-reanimated'; -import {ValueOf} from 'type-fest'; import Checkbox from '@components/Checkbox'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; @@ -17,25 +15,19 @@ import type ChatListItem from '@components/SelectionList/ChatListItem'; import type TaskListItem from '@components/SelectionList/Search/TaskListItem'; import type TransactionGroupListItem from '@components/SelectionList/Search/TransactionGroupListItem'; import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; -import type { - ExtendedTargetedEvent, - ReportActionListItemType, - SortableColumnName, - TaskListItemType, - TransactionGroupListItemType, - TransactionListItemType, -} from '@components/SelectionList/types'; +import type {ExtendedTargetedEvent, ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; import Text from '@components/Text'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useInitialWindowDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; -import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {isMobileChrome} from '@libs/Browser'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import variables from '@styles/variables'; @@ -87,28 +79,17 @@ type SearchListProps = Pick, 'onScroll' | 'conten /** The search query */ queryJSON: SearchQueryJSON; - columns?: SortableColumnName[]; - /** Called when the viewability of rows changes, as defined by the viewabilityConfig prop. */ onViewableItemsChanged?: (info: {changed: ViewToken[]; viewableItems: ViewToken[]}) => void; /** Invoked on mount and layout changes */ onLayout?: () => void; -<<<<<<< HEAD - /** Whether mobile selection mode is enabled */ - isMobileSelectionModeEnabled: boolean; -}; - -const keyExtractor = (item: SearchListItem, index: number) => { - return item.keyForList ?? `${index}`; -======= /** Styles to apply to the content container */ contentContainerStyle?: StyleProp; /** The estimated height of an item in the list */ estimatedItemSize?: number; ->>>>>>> @perunt/expenses-list-perf-on-web }; const keyExtractor = (item: SearchListItem, index: number) => item.keyForList ?? `${index}`; @@ -134,7 +115,6 @@ function SearchList( columns, onViewableItemsChanged, onLayout, - isMobileSelectionModeEnabled, estimatedItemSize = ITEM_HEIGHTS.NARROW_WITHOUT_DRAWER.STANDARD, }: SearchListProps, ref: ForwardedRef, @@ -172,7 +152,12 @@ function SearchList( const {isSmallScreenWidth, isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); + const {selectionMode} = useMobileSelectionMode(); const [longPressedItem, setLongPressedItem] = useState(); + // Check if selection should be on when the modal is opened + const wasSelectionOnRef = useRef(false); + // Keep track of the number of selected items to determine if we should turn off selection mode + const selectionRef = useRef(0); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { canBeMissing: true, @@ -180,6 +165,38 @@ function SearchList( const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); + useEffect(() => { + selectionRef.current = selectedItemsLength; + + if (!isSmallScreenWidth) { + if (selectedItemsLength === 0) { + turnOffMobileSelectionMode(); + } + return; + } + if (!isFocused) { + return; + } + if (!wasSelectionOnRef.current && selectedItemsLength > 0) { + wasSelectionOnRef.current = true; + } + if (selectedItemsLength > 0 && !selectionMode?.isEnabled) { + turnOnMobileSelectionMode(); + } else if (selectedItemsLength === 0 && selectionMode?.isEnabled && !wasSelectionOnRef.current) { + turnOffMobileSelectionMode(); + } + }, [selectionMode, isSmallScreenWidth, isFocused, selectedItemsLength]); + + useEffect( + () => () => { + if (selectionRef.current !== 0) { + return; + } + turnOffMobileSelectionMode(); + }, + [], + ); + const handleLongPressRow = useCallback( (item: SearchListItem) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -190,14 +207,14 @@ function SearchList( if ('transactions' in item && item.transactions.length === 0) { return; } - if (isMobileSelectionModeEnabled) { + if (selectionMode?.isEnabled) { onCheckboxPress(item); return; } setLongPressedItem(item); setIsModalVisible(true); }, - [isFocused, isSmallScreenWidth, onCheckboxPress, isMobileSelectionModeEnabled, shouldPreventLongPressRow], + [isFocused, isSmallScreenWidth, onCheckboxPress, selectionMode?.isEnabled, shouldPreventLongPressRow], ); const turnOnSelectionMode = useCallback(() => { @@ -344,8 +361,8 @@ function SearchList( }} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} queryJSONHash={hash} - policies={policies} columns={columns} + policies={policies} isDisabled={isDisabled} allReports={allReports} groupBy={groupBy} @@ -363,10 +380,10 @@ function SearchList( policies, hash, groupBy, + columns, setFocusedIndex, shouldPreventDefaultFocusOnSelectRow, allReports, - columns, ], ); diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 94bcc4f85e2a..730d85cfd187 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,10 +1,9 @@ import {useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; -import {shallowEqual} from 'fast-equals'; -import {useIsFocused, useNavigation} from '@react-navigation/native'; import type {ContentStyle} from '@shopify/flash-list'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-native'; import {View} from 'react-native'; +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -62,14 +61,8 @@ import type {SearchColumnType, SearchParams, SearchQueryJSON, SelectedTransactio type SearchProps = { queryJSON: SearchQueryJSON; onSearchListScroll?: (event: NativeSyntheticEvent) => void; -<<<<<<< HEAD - contentContainerStyle?: StyleProp; searchResults?: SearchResults; -======= contentContainerStyle?: ContentStyle; - currentSearchResults?: SearchResults; - lastNonEmptySearchResults?: SearchResults; ->>>>>>> @perunt/expenses-list-perf-on-web handleSearch: (value: SearchParams) => void; isMobileSelectionModeEnabled: boolean; }; @@ -516,7 +509,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const columns = {...currentColumns}; if (!previousColumnsRef.current) { - return columns; + return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; } const previouslyShownColumns = Object.keys(previousColumnsRef.current).filter((col) => previousColumnsRef.current[col] && !columns[col]); @@ -651,51 +644,58 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return ( - - ) - } - contentContainerStyle={{...contentContainerStyle, ...styles.pb3}} - containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - onScroll={onSearchListScroll} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - ListFooterComponent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - queryJSON={queryJSON} - columns={columnsToShow} - onViewableItemsChanged={onViewableItemsChanged} - onLayout={onLayout} - isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} - /> + + + ) + } + contentContainerStyle={{...contentContainerStyle, ...styles.pb3}} + containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + onScroll={onSearchListScroll} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + ListFooterComponent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + queryJSON={queryJSON} + columns={columnsToShow} + onViewableItemsChanged={onViewableItemsChanged} + onLayout={onLayout} + isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} + /> + ); } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index fe98e7f6978e..42b52c12300b 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,6 +1,5 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; From f3bd7f64a97ec2e172384fec0beae9637dd954aa Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 14 Jul 2025 21:39:23 +0100 Subject: [PATCH 29/56] New animation approach --- src/components/Search/SearchList.tsx | 5 ++- src/components/Search/index.tsx | 47 ++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index 541cce6ef18a..e550c8986254 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -35,7 +35,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {createItemHeightCalculator} from './itemHeightCalculator'; import ITEM_HEIGHTS from './itemHeights'; -import type {SearchQueryJSON} from './types'; +import type {SearchColumnType, SearchQueryJSON} from './types'; const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); @@ -79,6 +79,9 @@ type SearchListProps = Pick, 'onScroll' | 'conten /** The search query */ queryJSON: SearchQueryJSON; + /** The columns to show in the list */ + columns?: SearchColumnType[]; + /** Called when the viewability of rows changes, as defined by the viewabilityConfig prop. */ onViewableItemsChanged?: (info: {changed: ViewToken[]; viewableItems: ViewToken[]}) => void; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 730d85cfd187..74dc745720f2 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -3,7 +3,7 @@ import type {ContentStyle} from '@shopify/flash-list'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-native'; import {View} from 'react-native'; -import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -190,6 +190,35 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const {translate} = useLocalize(); const searchListRef = useRef(null); + // Custom animation for fade effect + const opacity = useSharedValue(1); + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + // Function to trigger fade animation manually + const triggerFadeAnimation = useCallback(() => { + console.log('animating'); + opacity.value = withTiming(0, {duration: 300}, () => { + opacity.value = withTiming(1, {duration: 300}); + }); + }, [opacity]); + + // Track when to trigger animation + const animationTrigger = useMemo(() => JSON.stringify(queryJSON) + JSON.stringify(searchResults?.search.columnsToShow), [queryJSON, searchResults?.search.columnsToShow]); + const previousAnimationTrigger = usePrevious(animationTrigger); + + useEffect(() => { + triggerFadeAnimation(); + }, [triggerFadeAnimation]); + + // Trigger animation when query or columns change + useEffect(() => { + if (previousAnimationTrigger && previousAnimationTrigger !== animationTrigger) { + triggerFadeAnimation(); + } + }, [animationTrigger, previousAnimationTrigger, triggerFadeAnimation]); + useFocusEffect( useCallback(() => { clearSelectedTransactions(hash); @@ -589,7 +618,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS ); }, [clearSelectedTransactions, data, groupBy, reportActionsArray, selectedTransactions, setSelectedTransactions]); - const onLayout = useCallback(() => handleSelectionListScroll(sortedSelectedData, searchListRef.current), [handleSelectionListScroll, sortedSelectedData]); + const onLayoutWithScrollRestore = useCallback(() => { + handleSelectionListScroll(sortedSelectedData, searchListRef.current); + // Restore scroll position after layout if needed + // Removed scroll position restoration logic + }, [handleSelectionListScroll, sortedSelectedData]); if (shouldShowLoadingState) { return ( @@ -644,12 +677,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return ( - + From 3991f81f909ff0fdda834587097305f992005503 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 14 Jul 2025 22:49:09 +0100 Subject: [PATCH 30/56] Add entering/exiting animations --- src/components/Search/index.tsx | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 043b01a4fd08..9f2cbb6675fe 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -3,7 +3,7 @@ import type {ContentStyle} from '@shopify/flash-list'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-native'; import {View} from 'react-native'; -import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; @@ -192,34 +192,29 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const searchListRef = useRef(null); // Custom animation for fade effect - const opacity = useSharedValue(1); + const opacity = useSharedValue(0); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, })); // Function to trigger fade animation manually - const triggerFadeAnimation = useCallback(() => { - console.log('animating'); - opacity.value = withTiming(0, {duration: 300}, () => { - opacity.value = withTiming(1, {duration: 300}); - }); - }, [opacity]); - - // Track when to trigger animation - const animationTrigger = useMemo(() => JSON.stringify(queryJSON) + JSON.stringify(searchResults?.search.columnsToShow), [queryJSON, searchResults?.search.columnsToShow]); - const previousAnimationTrigger = usePrevious(animationTrigger); + const triggerFadeAnimation = useCallback( + (initial = false) => { + if (initial) { + opacity.value = withTiming(1, {duration: 300}); + return; + } + opacity.value = withTiming(0, {duration: 300}, () => { + opacity.value = withTiming(1, {duration: 300}); + }); + }, + [opacity], + ); useEffect(() => { - triggerFadeAnimation(); + triggerFadeAnimation(true); }, [triggerFadeAnimation]); - // Trigger animation when query or columns change - useEffect(() => { - if (previousAnimationTrigger && previousAnimationTrigger !== animationTrigger) { - triggerFadeAnimation(); - } - }, [animationTrigger, previousAnimationTrigger, triggerFadeAnimation]); - useFocusEffect( useCallback(() => { clearSelectedTransactions(hash); @@ -549,8 +544,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (shallowCompare(currentColumns, previousColumnsRef.current)) { return; } + triggerFadeAnimation(); previousColumnsRef.current = {...currentColumns}; - }, [currentColumns]); + }, [currentColumns, triggerFadeAnimation]); const columnsToShow = useMemo(() => { const columns = {...currentColumns}; @@ -695,7 +691,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return ( - + Date: Mon, 14 Jul 2025 22:56:17 +0100 Subject: [PATCH 31/56] Fix double animation --- src/components/Search/index.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9f2cbb6675fe..01aad6948718 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -53,6 +53,7 @@ import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; import type SearchResults from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import arraysEqual from '@src/utils/arraysEqual'; import {useSearchContext} from './SearchContext'; import SearchList from './SearchList'; import SearchScopeProvider from './SearchScopeProvider'; @@ -538,16 +539,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS }, [searchResults?.data]); const previousColumnsRef = useRef>({}); - - useEffect(() => { - // Only update if columns actually changed - if (shallowCompare(currentColumns, previousColumnsRef.current)) { - return; - } - triggerFadeAnimation(); - previousColumnsRef.current = {...currentColumns}; - }, [currentColumns, triggerFadeAnimation]); - const columnsToShow = useMemo(() => { const columns = {...currentColumns}; @@ -564,6 +555,22 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; }, [currentColumns]); + useEffect(() => { + // Only update if columns actually changed + if (shallowCompare(currentColumns, previousColumnsRef.current)) { + return; + } + previousColumnsRef.current = {...currentColumns}; + }, [currentColumns, triggerFadeAnimation]); + + const previousColumnsToShow = usePrevious(columnsToShow); + useEffect(() => { + if (!previousColumnsToShow || arraysEqual(columnsToShow, previousColumnsToShow)) { + return; + } + triggerFadeAnimation(); + }, [previousColumnsToShow, columnsToShow, triggerFadeAnimation]); + const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || isMobileSelectionModeEnabled); @@ -739,6 +746,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS columns={columnsToShow} onViewableItemsChanged={onViewableItemsChanged} onLayout={onLayoutWithScrollRestore} + isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} /> From 548b7c2ec4b9a9a2b6b7a4377ea1f2bb546afcae Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 15 Jul 2025 01:05:18 +0100 Subject: [PATCH 32/56] Update animation --- src/components/Search/index.tsx | 75 +++++++++++++++------------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 01aad6948718..479fc74ee2a7 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -192,30 +192,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const {translate} = useLocalize(); const searchListRef = useRef(null); - // Custom animation for fade effect - const opacity = useSharedValue(0); - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - // Function to trigger fade animation manually - const triggerFadeAnimation = useCallback( - (initial = false) => { - if (initial) { - opacity.value = withTiming(1, {duration: 300}); - return; - } - opacity.value = withTiming(0, {duration: 300}, () => { - opacity.value = withTiming(1, {duration: 300}); - }); - }, - [opacity], - ); - - useEffect(() => { - triggerFadeAnimation(true); - }, [triggerFadeAnimation]); - useFocusEffect( useCallback(() => { clearSelectedTransactions(hash); @@ -531,16 +507,14 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [shouldShowLoadingState, isFocused], ); + const previousColumnsRef = useRef(null); + + // If a column was previously shown, keep it show const currentColumns = useMemo(() => { if (!searchResults?.data) { - return {}; + return []; } - return getColumnsToShow(searchResults?.data); - }, [searchResults?.data]); - - const previousColumnsRef = useRef>({}); - const columnsToShow = useMemo(() => { - const columns = {...currentColumns}; + const columns = getColumnsToShow(searchResults?.data); if (!previousColumnsRef.current) { return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; @@ -553,23 +527,44 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS }); return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; - }, [currentColumns]); + }, [searchResults?.data]); + // Only update if columns actually changed useEffect(() => { - // Only update if columns actually changed - if (shallowCompare(currentColumns, previousColumnsRef.current)) { + if (previousColumnsRef.current && arraysEqual(currentColumns, previousColumnsRef.current)) { return; } - previousColumnsRef.current = {...currentColumns}; - }, [currentColumns, triggerFadeAnimation]); + previousColumnsRef.current = currentColumns; + }, [currentColumns]); - const previousColumnsToShow = usePrevious(columnsToShow); + const previousColumns = usePrevious(currentColumns); + const [columnsToShow, setColumnsToShow] = useState([]); + + // Custom animation for fade effect + const opacity = useSharedValue(1); + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + const isAnimating = useSharedValue(false); useEffect(() => { - if (!previousColumnsToShow || arraysEqual(columnsToShow, previousColumnsToShow)) { + if (isAnimating.get()) { + return; + } + if (previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) { + setColumnsToShow(currentColumns); return; } - triggerFadeAnimation(); - }, [previousColumnsToShow, columnsToShow, triggerFadeAnimation]); + + isAnimating.set(true); + opacity.value = withTiming(0, {duration: 200}, (finished) => { + if (finished) { + isAnimating.set(false); + setColumnsToShow(currentColumns); + } + opacity.value = withTiming(1, {duration: 200}); + }); + }, [previousColumns, isAnimating, currentColumns, setColumnsToShow, opacity]); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; From ec6cac963b8a75976d9416544938559433f332c4 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 15 Jul 2025 23:20:57 +0100 Subject: [PATCH 33/56] Fix double animation --- src/components/Search/index.tsx | 52 ++++++++++++++++----------------- src/libs/SearchUIUtils.ts | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 479fc74ee2a7..10b007d281b4 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -23,7 +23,6 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {shallowCompare} from '@libs/ObjectUtils'; import Performance from '@libs/Performance'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils'; @@ -507,26 +506,22 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [shouldShowLoadingState, isFocused], ); - const previousColumnsRef = useRef(null); - - // If a column was previously shown, keep it show + // If a column was previously shown, keep it shown + const previousColumnsRef = useRef(null); const currentColumns = useMemo(() => { if (!searchResults?.data) { return []; } const columns = getColumnsToShow(searchResults?.data); - if (!previousColumnsRef.current) { - return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; - } - - const previouslyShownColumns = Object.keys(previousColumnsRef.current).filter((col) => previousColumnsRef.current[col] && !columns[col]); - - previouslyShownColumns.forEach((col) => { + (Object.keys(columns) as SearchColumnType[]).forEach((col) => { + if (!previousColumnsRef.current?.includes(col)) { + return; + } columns[col] = true; }); - return Object.keys(columns).filter((col) => columns[col]) as SearchColumnType[]; + return (Object.keys(columns) as SearchColumnType[]).filter((col) => columns[col]); }, [searchResults?.data]); // Only update if columns actually changed @@ -534,37 +529,42 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (previousColumnsRef.current && arraysEqual(currentColumns, previousColumnsRef.current)) { return; } - previousColumnsRef.current = currentColumns; + previousColumnsRef.current = [...currentColumns]; }, [currentColumns]); - const previousColumns = usePrevious(currentColumns); - const [columnsToShow, setColumnsToShow] = useState([]); - // Custom animation for fade effect const opacity = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, + opacity: opacity.get(), })); + const previousColumns = usePrevious(currentColumns); + const [columnsToShow, setColumnsToShow] = useState([]); const isAnimating = useSharedValue(false); + + // If columns have changed, trigger an animation before settings columnsToShow to prevent + // new columns appearing before the fade out animation happens useEffect(() => { if (isAnimating.get()) { return; } - if (previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) { + + if ((previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) || offset === 0) { setColumnsToShow(currentColumns); return; } isAnimating.set(true); - opacity.value = withTiming(0, {duration: 200}, (finished) => { - if (finished) { - isAnimating.set(false); - setColumnsToShow(currentColumns); - } - opacity.value = withTiming(1, {duration: 200}); - }); - }, [previousColumns, isAnimating, currentColumns, setColumnsToShow, opacity]); + opacity.set( + withTiming(0, {duration: 200}, (finished) => { + if (finished) { + isAnimating.set(false); + setColumnsToShow(currentColumns); + } + opacity.set(withTiming(1, {duration: 200})); + }), + ); + }, [previousColumns, isAnimating, currentColumns, setColumnsToShow, opacity, offset]); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 6bdd019079c2..48a02feaf7de 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1094,7 +1094,7 @@ function getSortedSections( * Determines what columns to show based on available data * @param isExpenseReportView: true when we are inside an expense report view, false if we're in the Reports page. */ -function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], isExpenseReportView = false) { +function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], isExpenseReportView = false): Record { const columns: Record = isExpenseReportView ? { [CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT]: true, From ffb16a0893427901eeea0bb388090ed0ad2b9ce1 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 16 Jul 2025 21:31:20 +0100 Subject: [PATCH 34/56] Fix flashlist not re-rendering when columns change --- src/components/Search/SearchList.tsx | 2 +- src/components/Search/index.tsx | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index cb0ebecdc905..72d38d32887e 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -483,7 +483,7 @@ function SearchList( onScroll={onScroll} showsVerticalScrollIndicator={false} ref={listRef} - extraData={focusedIndex} + extraData={[focusedIndex, columns]} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} ListFooterComponent={ListFooterComponent} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 10b007d281b4..9e5d03fd948d 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -545,10 +545,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS // If columns have changed, trigger an animation before settings columnsToShow to prevent // new columns appearing before the fade out animation happens useEffect(() => { - if (isAnimating.get()) { - return; - } - if ((previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) || offset === 0) { setColumnsToShow(currentColumns); return; From 3f63431e90cfc9c5365afd41d5735dca2ba86f70 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 20:30:55 +0100 Subject: [PATCH 35/56] Bug fix and reduce animation duration --- src/components/Search/index.tsx | 4 ++-- .../Search/TransactionGroupListItem.tsx | 21 ++----------------- src/components/SelectionList/types.ts | 1 + 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9e5d03fd948d..80c1d047b358 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -552,12 +552,12 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS isAnimating.set(true); opacity.set( - withTiming(0, {duration: 200}, (finished) => { + withTiming(0, {duration: 100}, (finished) => { if (finished) { isAnimating.set(false); setColumnsToShow(currentColumns); } - opacity.set(withTiming(1, {duration: 200})); + opacity.set(withTiming(1, {duration: 100})); }), ); }, [previousColumns, isAnimating, currentColumns, setColumnsToShow, opacity, offset]); diff --git a/src/components/SelectionList/Search/TransactionGroupListItem.tsx b/src/components/SelectionList/Search/TransactionGroupListItem.tsx index 172b71915bdb..ed935018a409 100644 --- a/src/components/SelectionList/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionList/Search/TransactionGroupListItem.tsx @@ -41,6 +41,7 @@ function TransactionGroupListItem({ onFocus, onLongPressRow, shouldSyncFocus, + columns, groupBy, policies, }: TransactionGroupListItemProps) { @@ -95,24 +96,6 @@ function TransactionGroupListItem({ }); }; - const sampleTransaction = groupItem.transactions.at(0); - const {COLUMNS} = CONST.REPORT.TRANSACTION_LIST; - - const columns = [ - COLUMNS.RECEIPT, - COLUMNS.TYPE, - COLUMNS.DATE, - COLUMNS.MERCHANT, - ...(sampleTransaction?.shouldShowDescription ? [COLUMNS.DESCRIPTION] : []), - ...(sampleTransaction?.shouldShowFrom ? [COLUMNS.FROM] : []), - ...(sampleTransaction?.shouldShowTo ? [COLUMNS.TO] : []), - ...(sampleTransaction?.shouldShowCategory ? [COLUMNS.CATEGORY] : []), - ...(sampleTransaction?.shouldShowTag ? [COLUMNS.TAG] : []), - ...(sampleTransaction?.shouldShowTax ? [COLUMNS.TAX] : []), - COLUMNS.TOTAL_AMOUNT, - COLUMNS.ACTION, - ] satisfies Array>; - const getHeader = (isHovered: boolean) => { const headers: Record = { [CONST.SEARCH.GROUP_BY.REPORTS]: ( @@ -199,7 +182,7 @@ function TransactionGroupListItem({ shouldUseNarrowLayout={!isLargeScreenWidth} shouldShowCheckbox={!!canSelectMultiple} onCheckboxPress={() => onCheckboxPress?.(transaction as unknown as TItem)} - columns={columns} + columns={columns as Array>} onButtonPress={() => { openReportInRHP(transaction); }} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index b96dae7c975f..b63203df8602 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -477,6 +477,7 @@ type TaskListItemProps = ListItemProps & { type TransactionGroupListItemProps = ListItemProps & { groupBy?: SearchGroupBy; policies?: OnyxCollection; + columns?: SortableColumnName[]; }; type ChatListItemProps = ListItemProps & { From 0455bb30e15fe192702882d0053dbf8b9205ce34 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 21:31:37 +0100 Subject: [PATCH 36/56] Remove unneeded variable --- src/components/Search/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 0062f23cdb51..6820286c9fc0 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -547,7 +547,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const previousColumns = usePrevious(currentColumns); const [columnsToShow, setColumnsToShow] = useState([]); - const isAnimating = useSharedValue(false); // If columns have changed, trigger an animation before settings columnsToShow to prevent // new columns appearing before the fade out animation happens @@ -557,17 +556,13 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } - isAnimating.set(true); opacity.set( - withTiming(0, {duration: 100}, (finished) => { - if (finished) { - isAnimating.set(false); - setColumnsToShow(currentColumns); - } + withTiming(0, {duration: 100}, () => { + setColumnsToShow(currentColumns); opacity.set(withTiming(1, {duration: 100})); }), ); - }, [previousColumns, isAnimating, currentColumns, setColumnsToShow, opacity, offset]); + }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset]); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; From 90c3f5c0d6a6a1cf4b728cfdd40c5585a9a73958 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 21:38:15 +0100 Subject: [PATCH 37/56] Fix lint --- src/components/Search/SearchList.tsx | 61 +++++++--------------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index 72d38d32887e..980f79d2174b 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -15,19 +15,25 @@ import type ChatListItem from '@components/SelectionList/ChatListItem'; import type TaskListItem from '@components/SelectionList/Search/TaskListItem'; import type TransactionGroupListItem from '@components/SelectionList/Search/TransactionGroupListItem'; import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; -import type {ExtendedTargetedEvent, ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type { + ExtendedTargetedEvent, + ReportActionListItemType, + SortableColumnName, + TaskListItemType, + TransactionGroupListItemType, + TransactionListItemType, +} from '@components/SelectionList/types'; import Text from '@components/Text'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useInitialWindowDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; -import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; -import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {isMobileChrome} from '@libs/Browser'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import variables from '@styles/variables'; @@ -35,7 +41,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {createItemHeightCalculator} from './itemHeightCalculator'; import ITEM_HEIGHTS from './itemHeights'; -import type {SearchColumnType, SearchQueryJSON} from './types'; +import type {SearchQueryJSON} from './types'; const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); @@ -79,8 +85,8 @@ type SearchListProps = Pick, 'onScroll' | 'conten /** The search query */ queryJSON: SearchQueryJSON; - /** The columns to show in the list */ - columns?: SearchColumnType[]; + /** Columns to show */ + columns: SortableColumnName[]; /** Called when the viewability of rows changes, as defined by the viewabilityConfig prop. */ onViewableItemsChanged?: (info: {changed: ViewToken[]; viewableItems: ViewToken[]}) => void; @@ -159,12 +165,7 @@ function SearchList( const {isSmallScreenWidth, isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); - const {selectionMode} = useMobileSelectionMode(); const [longPressedItem, setLongPressedItem] = useState(); - // Check if selection should be on when the modal is opened - const wasSelectionOnRef = useRef(false); - // Keep track of the number of selected items to determine if we should turn off selection mode - const selectionRef = useRef(0); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { canBeMissing: true, @@ -172,38 +173,6 @@ function SearchList( const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); - useEffect(() => { - selectionRef.current = selectedItemsLength; - - if (!isSmallScreenWidth) { - if (selectedItemsLength === 0) { - turnOffMobileSelectionMode(); - } - return; - } - if (!isFocused) { - return; - } - if (!wasSelectionOnRef.current && selectedItemsLength > 0) { - wasSelectionOnRef.current = true; - } - if (selectedItemsLength > 0 && !selectionMode?.isEnabled) { - turnOnMobileSelectionMode(); - } else if (selectedItemsLength === 0 && selectionMode?.isEnabled && !wasSelectionOnRef.current) { - turnOffMobileSelectionMode(); - } - }, [selectionMode, isSmallScreenWidth, isFocused, selectedItemsLength]); - - useEffect( - () => () => { - if (selectionRef.current !== 0) { - return; - } - turnOffMobileSelectionMode(); - }, - [], - ); - const handleLongPressRow = useCallback( (item: SearchListItem) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -214,14 +183,14 @@ function SearchList( if ('transactions' in item && item.transactions.length === 0) { return; } - if (selectionMode?.isEnabled) { + if (isMobileSelectionModeEnabled) { onCheckboxPress(item); return; } setLongPressedItem(item); setIsModalVisible(true); }, - [isFocused, isSmallScreenWidth, onCheckboxPress, selectionMode?.isEnabled, shouldPreventLongPressRow], + [isFocused, isSmallScreenWidth, onCheckboxPress, isMobileSelectionModeEnabled, shouldPreventLongPressRow], ); const turnOnSelectionMode = useCallback(() => { @@ -385,9 +354,9 @@ function SearchList( onCheckboxPress, onSelectRow, policies, + columns, hash, groupBy, - columns, setFocusedIndex, shouldPreventDefaultFocusOnSelectRow, allReports, From 2f8ed6ac4a1ac803258aa2bb4e70ed5fbbcae773 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 21:59:46 +0100 Subject: [PATCH 38/56] Fix TS --- .../MoneyRequestReportTransactionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index da05213ee298..49f1587f4132 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -177,7 +177,7 @@ function MoneyRequestReportTransactionList({ const columnsToShow = useMemo(() => { const columns = getColumnsToShow(transactions, true); - return Object.keys(columns).filter((column) => columns[column]) as SortableColumnName[]; + return (Object.keys(columns) as SortableColumnName[]).filter((column) => columns[column]); }, [transactions]); const navigateToTransaction = useCallback( From 8d88f74361211c12fdf7ddbde1640b032638972f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 22:01:22 +0100 Subject: [PATCH 39/56] Remove unneeded type change --- src/components/TransactionItemRow/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7c970ac26675..d9740cfa667a 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -47,7 +47,7 @@ import TypeCell from './DataCells/TypeCell'; import TransactionItemRowRBRWithOnyx from './TransactionItemRowRBRWithOnyx'; type ColumnComponents = { - [key in ValueOf]: React.ReactElement | null; + [key in ValueOf]: React.ReactElement; }; type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { From 0ef7bccb559ae789ea5385a8c7287da033dce5d4 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 22:15:01 +0100 Subject: [PATCH 40/56] TS fix --- tests/unit/useSearchHighlightAndScrollTest.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 248f901a657d..d0d45ab2964e 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -36,7 +36,14 @@ describe('useSearchHighlightAndScroll', () => { personalDetailsList: {}, }, search: { - columnsToShow: {shouldShowCategoryColumn: true, shouldShowTagColumn: true, shouldShowTaxColumn: true}, + columnsToShow: { + shouldShowCategoryColumn: true, + shouldShowTagColumn: true, + shouldShowTaxColumn: true, + shouldShowToColumn: true, + shouldShowFromColumn: true, + shouldShowDescriptionColumn: true, + }, hasMoreResults: false, hasResults: true, offset: 0, From f0f28c39e68e374bda6a4bfc1f28301d4f44ad93 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 23:38:57 +0100 Subject: [PATCH 41/56] Add test --- tests/unit/Search/SearchUIUtilsTest.ts | 176 +++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 32e7d767bb28..a952e6d5be51 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1556,4 +1556,180 @@ describe('SearchUIUtils', () => { expect(isAmountLengthLong3).toBe(true); expect(isTaxAmountLengthLong2).toBe(true); }); + + describe('Test getColumnsToShow', () => { + test('Should only show columns when at least one transaction has a value for them', () => { + // Use the existing transaction as a base and modify only the fields we need to test + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + + // Create test transactions as arrays (getColumnsToShow accepts arrays) + const emptyTransaction = { + ...baseTransaction, + transactionID: 'empty', + merchant: '', + modifiedMerchant: '', + comment: { comment: '' }, + category: '', + tag: '', + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const merchantTransaction = { + ...baseTransaction, + transactionID: 'merchant', + merchant: 'Test Merchant', + modifiedMerchant: '', + comment: { comment: '' }, + category: '', + tag: '', + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const categoryTransaction = { + ...baseTransaction, + transactionID: 'category', + merchant: '', + modifiedMerchant: '', + comment: { comment: '' }, + category: 'Office Supplies', + tag: '', + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const tagTransaction = { + ...baseTransaction, + transactionID: 'tag', + merchant: '', + modifiedMerchant: '', + comment: { comment: '' }, + category: '', + tag: 'Project A', + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const descriptionTransaction = { + ...baseTransaction, + transactionID: 'description', + merchant: '', + modifiedMerchant: '', + comment: { comment: 'Business meeting lunch' }, + category: '', + tag: '', + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const differentUsersTransaction = { + ...baseTransaction, + transactionID: 'differentUsers', + merchant: '', + modifiedMerchant: '', + comment: { comment: '' }, + category: '', + tag: '', + accountID: submitterAccountID, // Different from current user + managerID: approverAccountID, // Different from current user + }; + + // Test 1: No optional fields should be shown when all transactions are empty + let columns = SearchUIUtils.getColumnsToShow([emptyTransaction, emptyTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TAG]).toBe(false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]).toBe(false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.FROM]).toBe(false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TO]).toBe(false); + + // Test 2: Merchant column should show when at least one transaction has merchant + columns = SearchUIUtils.getColumnsToShow([emptyTransaction, merchantTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(false); + + // Test 3: Category column should show when at least one transaction has category + columns = SearchUIUtils.getColumnsToShow([emptyTransaction, categoryTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(false); + + // Test 4: Tag column should show when at least one transaction has tag + columns = SearchUIUtils.getColumnsToShow([emptyTransaction, tagTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TAG]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(false); + + // Test 5: Description column should show when at least one transaction has description + columns = SearchUIUtils.getColumnsToShow([emptyTransaction, descriptionTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(false); + + // Test 6: From/To columns should show when at least one transaction has different users + columns = SearchUIUtils.getColumnsToShow([emptyTransaction, differentUsersTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.FROM]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TO]).toBe(true); + + // Test 7: Multiple columns should show when transactions have different fields + columns = SearchUIUtils.getColumnsToShow([merchantTransaction, categoryTransaction, tagTransaction], false); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TAG]).toBe(true); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]).toBe(false); + }); + + test('Should respect isExpenseReportView flag and not show From/To columns', () => { + // Create transaction with different users using existing transaction as base + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + const testTransaction = { + ...baseTransaction, + transactionID: 'test', + merchant: 'Test Merchant', + modifiedMerchant: '', + comment: { comment: 'Test description' }, + category: 'Office Supplies', + tag: 'Project A', + accountID: submitterAccountID, // Different from current user + managerID: approverAccountID, // Different from current user + }; + + // In expense report view, From/To columns should not be shown + const columns = SearchUIUtils.getColumnsToShow([testTransaction], true); + + // These columns should be shown based on data + expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]).toBe(true); + expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY]).toBe(true); + expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG]).toBe(true); + expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]).toBe(true); + + // From/To columns should not exist in expense report view + expect(columns[CONST.SEARCH.TABLE_COLUMNS.FROM]).toBeUndefined(); + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TO]).toBeUndefined(); + }); + + test('Should handle modifiedMerchant and empty category/tag values correctly', () => { + const baseTransaction = searchResults.data[`transactions_${transactionID}`]; + const testTransaction = { + ...baseTransaction, + transactionID: 'modified', + merchant: '', + modifiedMerchant: 'Modified Merchant', + comment: { comment: '' }, + category: 'Uncategorized', // This is in CONST.SEARCH.CATEGORY_EMPTY_VALUE + tag: CONST.SEARCH.TAG_EMPTY_VALUE, // This is the empty tag value + accountID: adminAccountID, + managerID: adminAccountID, + }; + + const columns = SearchUIUtils.getColumnsToShow([testTransaction], false); + + // Should show merchant column because modifiedMerchant has value + expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(true); + + // Should not show category column because 'Uncategorized' is an empty value + expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(false); + + // Should not show tag column because it's the empty tag value + expect(columns[CONST.SEARCH.TABLE_COLUMNS.TAG]).toBe(false); + }); + }); }); From c3893f54d86f196e7dcf4452349533d37e3d7dc0 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 17 Jul 2025 23:39:14 +0100 Subject: [PATCH 42/56] Style --- tests/unit/Search/SearchUIUtilsTest.ts | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a952e6d5be51..4314d2ef93c5 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1561,14 +1561,14 @@ describe('SearchUIUtils', () => { test('Should only show columns when at least one transaction has a value for them', () => { // Use the existing transaction as a base and modify only the fields we need to test const baseTransaction = searchResults.data[`transactions_${transactionID}`]; - + // Create test transactions as arrays (getColumnsToShow accepts arrays) const emptyTransaction = { ...baseTransaction, transactionID: 'empty', merchant: '', modifiedMerchant: '', - comment: { comment: '' }, + comment: {comment: ''}, category: '', tag: '', accountID: adminAccountID, @@ -1580,7 +1580,7 @@ describe('SearchUIUtils', () => { transactionID: 'merchant', merchant: 'Test Merchant', modifiedMerchant: '', - comment: { comment: '' }, + comment: {comment: ''}, category: '', tag: '', accountID: adminAccountID, @@ -1592,7 +1592,7 @@ describe('SearchUIUtils', () => { transactionID: 'category', merchant: '', modifiedMerchant: '', - comment: { comment: '' }, + comment: {comment: ''}, category: 'Office Supplies', tag: '', accountID: adminAccountID, @@ -1604,7 +1604,7 @@ describe('SearchUIUtils', () => { transactionID: 'tag', merchant: '', modifiedMerchant: '', - comment: { comment: '' }, + comment: {comment: ''}, category: '', tag: 'Project A', accountID: adminAccountID, @@ -1616,7 +1616,7 @@ describe('SearchUIUtils', () => { transactionID: 'description', merchant: '', modifiedMerchant: '', - comment: { comment: 'Business meeting lunch' }, + comment: {comment: 'Business meeting lunch'}, category: '', tag: '', accountID: adminAccountID, @@ -1628,7 +1628,7 @@ describe('SearchUIUtils', () => { transactionID: 'differentUsers', merchant: '', modifiedMerchant: '', - comment: { comment: '' }, + comment: {comment: ''}, category: '', tag: '', accountID: submitterAccountID, // Different from current user @@ -1685,7 +1685,7 @@ describe('SearchUIUtils', () => { transactionID: 'test', merchant: 'Test Merchant', modifiedMerchant: '', - comment: { comment: 'Test description' }, + comment: {comment: 'Test description'}, category: 'Office Supplies', tag: 'Project A', accountID: submitterAccountID, // Different from current user @@ -1694,13 +1694,13 @@ describe('SearchUIUtils', () => { // In expense report view, From/To columns should not be shown const columns = SearchUIUtils.getColumnsToShow([testTransaction], true); - + // These columns should be shown based on data expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT]).toBe(true); expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY]).toBe(true); expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG]).toBe(true); expect(columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.DESCRIPTION]).toBe(true); - + // From/To columns should not exist in expense report view expect(columns[CONST.SEARCH.TABLE_COLUMNS.FROM]).toBeUndefined(); expect(columns[CONST.SEARCH.TABLE_COLUMNS.TO]).toBeUndefined(); @@ -1713,21 +1713,21 @@ describe('SearchUIUtils', () => { transactionID: 'modified', merchant: '', modifiedMerchant: 'Modified Merchant', - comment: { comment: '' }, - category: 'Uncategorized', // This is in CONST.SEARCH.CATEGORY_EMPTY_VALUE + comment: {comment: ''}, + category: 'Uncategorized', // This is in CONST.SEARCH.CATEGORY_EMPTY_VALUE tag: CONST.SEARCH.TAG_EMPTY_VALUE, // This is the empty tag value accountID: adminAccountID, managerID: adminAccountID, }; const columns = SearchUIUtils.getColumnsToShow([testTransaction], false); - + // Should show merchant column because modifiedMerchant has value expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(true); - + // Should not show category column because 'Uncategorized' is an empty value expect(columns[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]).toBe(false); - + // Should not show tag column because it's the empty tag value expect(columns[CONST.SEARCH.TABLE_COLUMNS.TAG]).toBe(false); }); From 45b4309890bddc85d7d6b8635f9161e9f38d479d Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 18 Jul 2025 00:04:39 +0100 Subject: [PATCH 43/56] Remove unused column --- src/libs/SearchUIUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 135e31a2e300..e6c1fb387fef 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1159,8 +1159,6 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy [CONST.SEARCH.TABLE_COLUMNS.TITLE]: true, [CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: true, [CONST.SEARCH.TABLE_COLUMNS.IN]: true, - // This column is never displayed on Search - [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: false, }; const updateColumns = (transaction: OnyxTypes.Transaction | SearchTransaction) => { From e00be418803da7f47492615ba41cf499f683ce84 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 18 Jul 2025 18:47:47 +0100 Subject: [PATCH 44/56] Allow tweaking animation duration via console temporarily --- src/components/Search/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 6820286c9fc0..9c6b4e205fb5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -557,9 +557,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } opacity.set( - withTiming(0, {duration: 100}, () => { + withTiming(0, {duration: window.animationDuration || 200}, () => { setColumnsToShow(currentColumns); - opacity.set(withTiming(1, {duration: 100})); + opacity.set(withTiming(1, {duration: window.animationDuration || 200})); }), ); }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset]); From f75a898da843e6de27c71f893aa6ec1d1fb6708f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 22 Jul 2025 20:33:27 +0100 Subject: [PATCH 45/56] Improve animation --- src/components/Search/index.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 015f2d8f5c23..02f9f96f20da 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -642,10 +642,16 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (shouldShowLoadingState) { return ( - + + + ); } @@ -694,8 +700,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return ( Date: Tue, 22 Jul 2025 21:14:16 +0100 Subject: [PATCH 46/56] Remove previous columns logic --- src/components/Search/index.tsx | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 02f9f96f20da..0c8e6e6d6d94 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -512,32 +512,15 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [shouldShowLoadingState, isFocused], ); - // If a column was previously shown, keep it shown - const previousColumnsRef = useRef(null); const currentColumns = useMemo(() => { if (!searchResults?.data) { return []; } const columns = getColumnsToShow(searchResults?.data); - (Object.keys(columns) as SearchColumnType[]).forEach((col) => { - if (!previousColumnsRef.current?.includes(col)) { - return; - } - columns[col] = true; - }); - return (Object.keys(columns) as SearchColumnType[]).filter((col) => columns[col]); }, [searchResults?.data]); - // Only update if columns actually changed - useEffect(() => { - if (previousColumnsRef.current && arraysEqual(currentColumns, previousColumnsRef.current)) { - return; - } - previousColumnsRef.current = [...currentColumns]; - }, [currentColumns]); - // Custom animation for fade effect const opacity = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => ({ @@ -556,9 +539,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } opacity.set( - withTiming(0, {duration: window.animationDuration || 200}, () => { + withTiming(0, {duration: 200}, () => { setColumnsToShow(currentColumns); - opacity.set(withTiming(1, {duration: window.animationDuration || 200})); + opacity.set(withTiming(1, {duration: 200})); }), ); }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset]); From fe32c987e3234d453f6daaff266a46b5638a5fdb Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 23 Jul 2025 18:53:18 +0100 Subject: [PATCH 47/56] Don't refetch results when scrolling back to top --- src/components/Search/index.tsx | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 0c8e6e6d6d94..3121bdaabcd3 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,7 +1,7 @@ import {useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; import type {ContentStyle} from '@shopify/flash-list'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-native'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; @@ -491,27 +491,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [hash, isMobileSelectionModeEnabled, toggleTransaction], ); - const onViewableItemsChanged = useCallback( - ({viewableItems}: {viewableItems: ViewToken[]}) => { - if (!isFocused) { - return; - } - - const isFirstItemVisible = viewableItems.at(0)?.index === 1; - // If the user is still loading the search results, or if they are scrolling down, don't refresh the search results - if (shouldShowLoadingState || !isFirstItemVisible) { - return; - } - - // This line makes sure the app refreshes the search results when the user scrolls to the top. - // The backend sends items in parts based on the offset, with a limit on the number of items sent (pagination). - // As a result, it skips some items, for example, if the offset is 100, it sends the next items without the first ones. - // Therefore, when the user scrolls to the top, we need to refresh the search results. - setOffset(0); - }, - [shouldShowLoadingState, isFocused], - ); - const currentColumns = useMemo(() => { if (!searchResults?.data) { return []; @@ -728,7 +707,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } queryJSON={queryJSON} columns={columnsToShow} - onViewableItemsChanged={onViewableItemsChanged} onLayout={onLayoutWithScrollRestore} isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} /> From b15ec1efb62140cca8878a5946d768e37b91c10b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 24 Jul 2025 19:19:45 +0100 Subject: [PATCH 48/56] Update src/types/onyx/SearchResults.ts Co-authored-by: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> --- src/types/onyx/SearchResults.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 18dc7c7a2741..04a216a493a1 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -35,10 +35,10 @@ type ListItemDataType = C ext /** Model of columns to show for search results */ type ColumnsToShow = { - /** Whether the From column show be shown */ + /** Whether the From column should be shown */ shouldShowFromColumn: boolean; - /** Whether the To column show be shown */ + /** Whether the To column should be shown */ shouldShowToColumn: boolean; /** Whether the category column should be shown */ From df7cf5a618aaf6b4021edfbb911a4b7d4fa9cda7 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 24 Jul 2025 19:23:41 +0100 Subject: [PATCH 49/56] Use constant for fade duration --- src/CONST/index.ts | 3 +++ src/components/Search/index.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c8c2b6bbcbae..55f0ca890173 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6449,6 +6449,9 @@ const CONST = { UNAPPROVED_CASH: 'unapprovedCash', UNAPPROVED_COMPANY_CARDS: 'unapprovedCompanyCards', }, + ANIMATION: { + FADE_DURATION: 200, + }, }, EXPENSE: { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 3121bdaabcd3..20d5209b2c14 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -518,9 +518,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } opacity.set( - withTiming(0, {duration: 200}, () => { + withTiming(0, {duration: CONST.SEARCH.ANIMATION.FADE_DURATION}, () => { setColumnsToShow(currentColumns); - opacity.set(withTiming(1, {duration: 200})); + opacity.set(withTiming(1, {duration: CONST.SEARCH.ANIMATION.FADE_DURATION})); }), ); }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset]); @@ -605,8 +605,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (shouldShowLoadingState) { return ( Date: Thu, 24 Jul 2025 19:25:24 +0100 Subject: [PATCH 50/56] Don't animate new columns on mobile --- src/components/Search/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 22095676b024..a07a2ba06115 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -538,7 +538,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS // If columns have changed, trigger an animation before settings columnsToShow to prevent // new columns appearing before the fade out animation happens useEffect(() => { - if ((previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) || offset === 0) { + if ((previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) || offset === 0 || isSmallScreenWidth) { setColumnsToShow(currentColumns); return; } @@ -549,7 +549,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS opacity.set(withTiming(1, {duration: CONST.SEARCH.ANIMATION.FADE_DURATION})); }), ); - }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset]); + }, [previousColumns, currentColumns, setColumnsToShow, opacity, offset, isSmallScreenWidth]); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; From 2ef632d2a0ac2ef25736ba997948c8f2b0c7433a Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 24 Jul 2025 20:22:07 +0100 Subject: [PATCH 51/56] Fix To column showing when managerID=0 --- src/libs/SearchUIUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 08e27ca8fa53..ee72f5430223 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1401,7 +1401,7 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy } const managerID = (transaction as SearchTransaction).managerID; - if (managerID !== currentAccountID) { + if (managerID && managerID !== currentAccountID) { columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] = true; } }; From 1c62e31be77b701c568e36dccf3b1a00fdf8de81 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 25 Jul 2025 21:24:11 +0100 Subject: [PATCH 52/56] Fix empty To column --- src/libs/SearchUIUtils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 65f0196b076d..ca162b973761 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -77,6 +77,7 @@ import { isInvoiceReport, isMoneyRequestReport, isOpenExpenseReport, + isOpenReport, isSettled, } from './ReportUtils'; import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, getTodoSearchQuery} from './SearchQueryUtils'; @@ -1368,7 +1369,7 @@ function getSortedSections( * Determines what columns to show based on available data * @param isExpenseReportView: true when we are inside an expense report view, false if we're in the Reports page. */ -function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], isExpenseReportView = false): Record { +function getColumnsToShow(data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[], isExpenseReportView = false): Record { const columns: Record = isExpenseReportView ? { [CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT]: true, @@ -1433,18 +1434,19 @@ function getColumnsToShow(transactions: OnyxTypes.SearchResults['data'] | OnyxTy const managerID = (transaction as SearchTransaction).managerID; if (managerID && managerID !== currentAccountID) { - columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] = true; + const report = (data as OnyxTypes.SearchResults['data'])[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; + columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] = !!report && !isOpenReport(report); } }; - if (Array.isArray(transactions)) { - transactions.forEach(updateColumns); + if (Array.isArray(data)) { + data.forEach(updateColumns); } else { - Object.keys(transactions).forEach((key) => { + Object.keys(data).forEach((key) => { if (!isTransactionEntry(key)) { return; } - updateColumns(transactions[key]); + updateColumns(data[key]); }); } From 0dd518bf493780ee2b173f76ce4959be516652c7 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 25 Jul 2025 23:42:21 +0100 Subject: [PATCH 53/56] Fix test --- tests/unit/Search/SearchUIUtilsTest.ts | 37 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 4bc4c2f037c3..252ec87cb15f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1556,7 +1556,9 @@ describe('SearchUIUtils', () => { }); describe('Test getColumnsToShow', () => { - test('Should only show columns when at least one transaction has a value for them', () => { + test('Should only show columns when at least one transaction has a value for them', async () => { + await Onyx.merge(ONYXKEYS.SESSION, {accountID: submitterAccountID}); + // Use the existing transaction as a base and modify only the fields we need to test const baseTransaction = searchResults.data[`transactions_${transactionID}`]; @@ -1569,8 +1571,8 @@ describe('SearchUIUtils', () => { comment: {comment: ''}, category: '', tag: '', - accountID: adminAccountID, - managerID: adminAccountID, + accountID: submitterAccountID, + managerID: submitterAccountID, }; const merchantTransaction = { @@ -1581,8 +1583,8 @@ describe('SearchUIUtils', () => { comment: {comment: ''}, category: '', tag: '', - accountID: adminAccountID, - managerID: adminAccountID, + accountID: submitterAccountID, + managerID: submitterAccountID, }; const categoryTransaction = { @@ -1593,8 +1595,8 @@ describe('SearchUIUtils', () => { comment: {comment: ''}, category: 'Office Supplies', tag: '', - accountID: adminAccountID, - managerID: adminAccountID, + accountID: submitterAccountID, + managerID: submitterAccountID, }; const tagTransaction = { @@ -1605,8 +1607,8 @@ describe('SearchUIUtils', () => { comment: {comment: ''}, category: '', tag: 'Project A', - accountID: adminAccountID, - managerID: adminAccountID, + accountID: submitterAccountID, + managerID: submitterAccountID, }; const descriptionTransaction = { @@ -1617,8 +1619,8 @@ describe('SearchUIUtils', () => { comment: {comment: 'Business meeting lunch'}, category: '', tag: '', - accountID: adminAccountID, - managerID: adminAccountID, + accountID: submitterAccountID, + managerID: submitterAccountID, }; const differentUsersTransaction = { @@ -1629,8 +1631,9 @@ describe('SearchUIUtils', () => { comment: {comment: ''}, category: '', tag: '', - accountID: submitterAccountID, // Different from current user - managerID: approverAccountID, // Different from current user + accountID: approverAccountID, // Different from current user + managerID: adminAccountID, // Different from current user + reportID: reportID2, // Needs to be a submitter report for 'To' to show }; // Test 1: No optional fields should be shown when all transactions are empty @@ -1663,7 +1666,13 @@ describe('SearchUIUtils', () => { expect(columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]).toBe(false); // Test 6: From/To columns should show when at least one transaction has different users - columns = SearchUIUtils.getColumnsToShow([emptyTransaction, differentUsersTransaction], false); + // @ts-expect-error -- no need to construct all data again, the function below only needs the report and transactions + const data: OnyxTypes.SearchResults['data'] = { + [`report_${reportID2}`]: searchResults.data[`report_${reportID2}`], + [`transactions_${emptyTransaction.transactionID}`]: emptyTransaction, + [`transactions_${differentUsersTransaction.transactionID}`]: differentUsersTransaction, + }; + columns = SearchUIUtils.getColumnsToShow(data, false); expect(columns[CONST.SEARCH.TABLE_COLUMNS.FROM]).toBe(true); expect(columns[CONST.SEARCH.TABLE_COLUMNS.TO]).toBe(true); From 20e8c8084cd283e7ec7aac30a0fc9fb82f1f61d1 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Sat, 26 Jul 2025 00:01:45 +0100 Subject: [PATCH 54/56] Fix value being overridden --- src/libs/SearchUIUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index ca162b973761..5ebc50482cf1 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1433,7 +1433,7 @@ function getColumnsToShow(data: OnyxTypes.SearchResults['data'] | OnyxTypes.Tran } const managerID = (transaction as SearchTransaction).managerID; - if (managerID && managerID !== currentAccountID) { + if (managerID && managerID !== currentAccountID && !columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO]) { const report = (data as OnyxTypes.SearchResults['data'])[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; columns[CONST.REPORT.TRANSACTION_LIST.COLUMNS.TO] = !!report && !isOpenReport(report); } From 8f24832d46a919ac060f65628ce7e474e2859f2c Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Sun, 27 Jul 2025 23:55:20 +0100 Subject: [PATCH 55/56] Fix TS --- .../MoneyRequestReportTransactionItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx index 432d5ec5d823..0528b7d7deed 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx @@ -1,5 +1,6 @@ import React, {useEffect, useRef} from 'react'; import type {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import {getButtonRole} from '@components/Button/utils'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; @@ -126,7 +127,7 @@ function MoneyRequestReportTransactionItem({ shouldUseNarrowLayout={shouldUseNarrowLayout || isMediumScreenWidth} shouldShowCheckbox={!!isSelectionModeEnabled || !isSmallScreenWidth} onCheckboxPress={toggleTransaction} - columns={columns} + columns={columns as Array>} isDisabled={isPendingDelete} /> From 2f9d48b91712694a928cf39f37019ddef14c6e0f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 29 Jul 2025 20:45:29 +0100 Subject: [PATCH 56/56] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lucien Akchoté --- src/components/Search/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index cefa232aafd4..38523f721f1a 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -693,8 +693,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return (