From b5e960d783b452916f4322cacd079c558b6e84f2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 25 Mar 2026 11:25:33 +0800 Subject: [PATCH 01/70] use the revamped search actions bar --- .../Search/SearchAutocompleteInput.tsx | 119 ++-- src/components/Search/SearchList/index.tsx | 2 +- .../SearchActionsBarCreateButton.tsx | 2 +- .../SearchActionsBarNarrow.tsx | 1 - .../SearchPageHeader/SearchActionsBarWide.tsx | 1 - .../SearchPageHeader/SearchFiltersBar.tsx | 28 - .../SearchFiltersBarNarrow.tsx | 107 --- .../SearchPageHeader/SearchFiltersBarWide.tsx | 105 --- .../SearchPageHeader/SearchPageHeader.tsx | 39 -- .../SearchPageHeaderInput.tsx | 400 ----------- .../SearchPageHeaderNarrow.tsx | 2 +- .../SearchPageHeader/SearchPageHeaderWide.tsx | 2 +- .../SearchPageInputNarrow.tsx | 1 - .../SearchPageHeader/SearchPageInputWide.tsx | 7 +- .../SearchPageHeader/useSearchFiltersBar.tsx | 663 ------------------ src/components/Search/index.tsx | 10 +- .../Skeletons/SearchActionsSkeleton.tsx | 2 +- src/hooks/useSearchFilterSync.ts | 2 +- .../Navigators/RightModalNavigator.tsx | 5 + src/pages/Search/SearchPage.tsx | 1 + src/pages/Search/SearchPageNarrow.tsx | 77 +- src/pages/Search/SearchPageWide.tsx | 14 +- src/styles/index.ts | 23 +- src/styles/variables.ts | 8 +- 24 files changed, 147 insertions(+), 1474 deletions(-) delete mode 100644 src/components/Search/SearchPageHeader/SearchFiltersBar.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchPageHeader.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx delete mode 100644 src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 28c2ad81da56..9d1b9917ea52 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -59,6 +59,10 @@ type SearchAutocompleteInputProps = { /** Any additional styles to apply to text input along with FormHelperMessage */ outerWrapperStyle?: StyleProp; + inputContainerStyle?: StyleProp; + + touchableInputWrapperStyle?: StyleProp; + /** Whether the search reports API call is running */ isSearchingForReports?: boolean; @@ -87,6 +91,8 @@ function SearchAutocompleteInput({ wrapperStyle, wrapperFocusedStyle = {}, outerWrapperStyle, + inputContainerStyle, + touchableInputWrapperStyle, isSearchingForReports, selection, substitutionMap, @@ -192,63 +198,62 @@ function SearchAutocompleteInput({ return ( - - - { - onFocus?.(); - focusedSharedValue.set(true); - }} - onBlur={() => { - focusedSharedValue.set(false); - onBlur?.(); - }} - onKeyPress={onKeyPress} - isLoading={isSearchingForReports} - ref={(element) => { - if (!ref) { - return; - } - - inputRef.current = element as AnimatedTextInputRef; - - if (typeof ref === 'function') { - ref(element); - return; - } - - // eslint-disable-next-line no-param-reassign - ref.current = element; - }} - type="markdown" - multiline={false} - parser={parser} - selection={selection} - shouldShowClearButton={!!value && !isSearchingForReports} - shouldHideClearButton={false} - onClearInput={clearInput} - /> - + + { + onFocus?.(); + focusedSharedValue.set(true); + }} + onBlur={() => { + focusedSharedValue.set(false); + onBlur?.(); + }} + onKeyPress={onKeyPress} + isLoading={isSearchingForReports} + ref={(element) => { + if (!ref) { + return; + } + + inputRef.current = element as AnimatedTextInputRef; + + if (typeof ref === 'function') { + ref(element); + return; + } + + // eslint-disable-next-line no-param-reassign + ref.current = element; + }} + type="markdown" + multiline={false} + parser={parser} + selection={selection} + shouldShowClearButton={!!value && !isSearchingForReports} + shouldHideClearButton={false} + onClearInput={clearInput} + /> 0 && selectedItemsLength === totalItems && hasLoadedAllTransactions; const content = ( - + {tableHeaderVisible && ( {canSelectMultiple && ( diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx index f6467e5d8429..01130c0ab065 100644 --- a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx @@ -225,7 +225,7 @@ function SearchActionsBarCreateButton() { ); return ( - + void; }; -// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876 function SearchActionsBarNarrow({queryJSON, isMobileSelectionModeEnabled, isSearchInputVisible, searchResults, onSearchButtonPress, onSort}: SearchActionsBarNarrowProps) { const {hasErrors, shouldShowActionsBarLoading, shouldShowSelectedDropdown, styles} = useSearchActionsBar(queryJSON, isMobileSelectionModeEnabled); const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass']); diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx index 19a074780a63..88617bf5d734 100644 --- a/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx +++ b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx @@ -71,7 +71,6 @@ const FILTER_KEY_TO_COMPONENT: Partial - ) : ( - - ); -} - -export default SearchFiltersBar; diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx deleted file mode 100644 index d48c155314ae..000000000000 --- a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, {useRef} from 'react'; -import {FlatList, View} from 'react-native'; -import Button from '@components/Button'; -import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton'; -import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton'; -import type {SearchQueryJSON} from '@components/Search/types'; -import SearchActionsSkeleton from '@components/Skeletons/SearchActionsSkeleton'; -import shouldAdjustScroll from '@libs/shouldAdjustScroll'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import CONST from '@src/CONST'; -import type {FilterItem} from './useSearchFiltersBar'; -import useSearchFiltersBar from './useSearchFiltersBar'; - -type SearchFiltersBarNarrowProps = { - queryJSON: SearchQueryJSON; - isMobileSelectionModeEnabled: boolean; -}; - -function SearchFiltersBarNarrow({queryJSON, isMobileSelectionModeEnabled}: SearchFiltersBarNarrowProps) { - const scrollRef = useRef>(null); - const {filters, hasErrors, shouldShowFiltersBarLoading, shouldShowSelectedDropdown, filterButtonText, openAdvancedFilters, expensifyIcons, theme, styles} = useSearchFiltersBar( - queryJSON, - isMobileSelectionModeEnabled, - ); - - const adjustScroll = (info: {distanceFromEnd: number}) => { - // Workaround for a known React Native bug on Android (https://github.com/facebook/react-native/issues/27504): - // When the FlatList is scrolled to the end and the last item is deleted, a blank space is left behind. - // To fix this, we detect when onEndReached is triggered due to an item deletion, - // and programmatically scroll to the end to fill the space. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!shouldAdjustScroll || info.distanceFromEnd > 0) { - return; - } - scrollRef.current?.scrollToEnd(); - }; - - const renderFilterItem = ({item}: {item: FilterItem}) => ( - - ); - - const renderListFooter = () => ( - - - ); -} - -/** - * In the submit-and-navigate flow we only ever land on `type:expense` or `type:invoice` - * with default status and no extra filters, so the chips are mostly hardcoded. - * The only conditional chip is "Workspaces" (shown when the user has >1 workspace), - * resolved via a cheap boolean Onyx selector. - */ -function StaticFiltersBar({queryJSON}: {queryJSON: SearchQueryJSON}) { - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter'] as const); - const [policyInfo] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: staticPolicyInfoSelector}); - const hasMultipleWorkspaces = policyInfo?.hasMultipleWorkspaces ?? false; - - const typeLabel = queryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE ? translate('common.invoice') : translate('common.expense'); - - const chips = useMemo( - () => [ - {key: 'type', label: `${translate('common.type')}: ${typeLabel}`}, - {key: 'status', label: translate('common.status')}, - {key: 'date', label: translate('common.date')}, - {key: 'from', label: translate('common.from')}, - ...(hasMultipleWorkspaces ? [{key: 'workspace', label: translate('workspace.common.workspace')}] : []), - ], - [translate, typeLabel, hasMultipleWorkspaces], - ); - - return ( - - item.key} - renderItem={({item}) => } - ListFooterComponent={ - + + {buttonText} + + + + )} {/* Dropdown overlay */} - + ); } diff --git a/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx b/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx index e997abc04b07..dd67f0775ae6 100644 --- a/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx @@ -1,20 +1,15 @@ -import React, {useState} from 'react'; -import type {ReactNode} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components/Icon'; -import Modal from '@components/Modal'; -import {PressableWithoutFeedback} from '@components/Pressable'; +import {PressableWithFeedback} from '@components/Pressable'; import DisplayPopup from '@components/Search/FilterDropdowns/DisplayPopup'; import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton'; import type {SearchQueryJSON} from '@components/Search/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type {SearchResults} from '@src/types/onyx'; @@ -24,84 +19,52 @@ type SearchDisplayDropdownButtonProps = { onSort: () => void; }; -type DisplayIconButtonProps = { - ModalComponent: ({closeOverlay}: {closeOverlay: () => void}) => ReactNode; -}; - -function DisplayIconButton({ModalComponent}: DisplayIconButtonProps) { +function SearchDisplayDropdownButton({queryJSON, searchResults, onSort}: SearchDisplayDropdownButtonProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Gear']); const theme = useTheme(); const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowHeight} = useWindowDimensions(); - const viewportOffsetTop = useViewportOffsetTop(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Gear']); - - const [isModalVisible, setIsModalVisible] = useState(false); - - return ( - <> - setIsModalVisible(true)} - > - - - setIsModalVisible(false)} - animationIn="slideInUp" - animationOut="slideOutDown" - shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode={false} - innerContainerStyle={styles.w100} - outerStyle={{...StyleUtils.getOuterModalStyle(windowHeight, viewportOffsetTop), ...styles.w100}} - > - {ModalComponent({closeOverlay: () => setIsModalVisible(false)})} - - - ); -} - -function SearchDisplayDropdownButton({queryJSON, searchResults, onSort}: SearchDisplayDropdownButtonProps) { - const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); if (queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT) { return null; } - const displayPopup = ({closeOverlay}: {closeOverlay: () => void}) => ( - - ); - - if (shouldUseNarrowLayout) { - return ; - } - return ( + PopoverComponent={({closeOverlay}) => ( + + )} + > + {shouldUseNarrowLayout || isMediumScreenWidth + ? (triggerRef, onPress) => ( + + + + ) + : undefined} + ); } diff --git a/src/components/Search/SearchPageHeader/SearchSaveButton.tsx b/src/components/Search/SearchPageHeader/SearchSaveButton.tsx index dc2fdec35f4d..1dcf5b96ca64 100644 --- a/src/components/Search/SearchPageHeader/SearchSaveButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchSaveButton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Button from '@components/Button'; import Icon from '@components/Icon'; -import {PressableWithoutFeedback} from '@components/Pressable'; +import {PressableWithFeedback} from '@components/Pressable'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -15,28 +15,30 @@ function SearchSaveButton() { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bookmark']); const openSaveSearchPage = () => { Navigation.navigate(ROUTES.SEARCH_SAVE); }; - if (shouldUseNarrowLayout) { + if (shouldUseNarrowLayout || isMediumScreenWidth) { return ( - - + ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index 1d759952031a..3c15a5d448b3 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -6370,6 +6370,15 @@ const dynamicStyles = (theme: ThemeColors) => height: shouldUseNarrowLayout ? variables.sectionMenuItemHeight : variables.sectionMenuItemHeightCompact, alignItems: 'center', }), + + searchActionsBar: (shouldUseNarrowLayout: boolean) => ({ + alignItems: 'center', + height: shouldUseNarrowLayout ? variables.componentSizeNormal : variables.h28, + justifyContent: 'center', + width: shouldUseNarrowLayout ? variables.componentSizeNormal : variables.h28, + backgroundColor: shouldUseNarrowLayout ? undefined : theme.buttonDefaultBG, + borderRadius: 999, + }), }) satisfies DynamicStyles; // Styles that cannot be wrapped in StyleSheet.create because they eg. must be passed to 3rd party libraries as JS objects From 2b7f406d07d2978bb8c4dbffcddc5bc49e88b078 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Apr 2026 11:58:38 +0800 Subject: [PATCH 70/70] lint --- .../MoneyRequestReportTransactionList.tsx | 2 +- .../Search/FilterDropdowns/DropdownButton.tsx | 11 +++- .../SearchDisplayDropdownButton.tsx | 62 ++++++++++--------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 10d56949ee0c..963cbfc5eceb 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -513,7 +513,7 @@ function MoneyRequestReportTransactionList({ ), - [groupByOptions, reportLayoutGroupBy, styles, windowHeight, isInLandscapeMode], + [groupByOptions, reportLayoutGroupBy, styles, windowHeight, isSmallScreenWidth, isInLandscapeMode], ); const transactionListContent = ( diff --git a/src/components/Search/FilterDropdowns/DropdownButton.tsx b/src/components/Search/FilterDropdowns/DropdownButton.tsx index 7c56168c4880..4e835a205266 100644 --- a/src/components/Search/FilterDropdowns/DropdownButton.tsx +++ b/src/components/Search/FilterDropdowns/DropdownButton.tsx @@ -40,6 +40,8 @@ type DropdownButtonProps = WithSentryLabel & { /** The component to render in the popover */ PopoverComponent: (props: PopoverComponentProps) => ReactNode; + ButtonComponent?: React.ComponentType<{onPress: () => void; ref: RefObject}>; + /** Whether to use medium size button instead of small */ medium?: boolean; @@ -62,11 +64,11 @@ const ANCHOR_ORIGIN = { }; function DropdownButton({ - children, label, value, viewportOffsetTop, PopoverComponent, + ButtonComponent, medium = false, labelStyle, innerStyles, @@ -148,8 +150,11 @@ function DropdownButton({ style={wrapperStyle} > {/* Dropdown Trigger */} - {children ? ( - children(triggerRef, calculatePopoverPositionAndToggleOverlay) + {ButtonComponent ? ( + ) : (