From a8236c7bdceb723a6e6b72d2d356c36ef3c448c4 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 18 Sep 2024 12:35:17 +0200 Subject: [PATCH 1/8] Prefill Search Router with existing Search query --- .../Search/SearchRouter/SearchRouter.tsx | 51 ++++++++++++++++--- .../Search/SearchRouter/SearchRouterInput.tsx | 5 +- .../BottomTabBar.tsx | 11 ++-- src/libs/SearchUtils.ts | 45 ++++++++++++---- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 63699d34ce04..d94acc2baec6 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import debounce from 'lodash/debounce'; import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; @@ -9,22 +10,40 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; +import type {AuthScreensParamList, NavigationStateRoute} from '@navigation/types'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import {useSearchRouterContext} from './SearchRouterContext'; import SearchRouterInput from './SearchRouterInput'; const SEARCH_DEBOUNCE_DELAY = 200; +function getCurrentSearchQuery(route?: NavigationStateRoute) { + if (route?.name !== SCREENS.SEARCH.CENTRAL_PANE) { + return; + } + + const query = (route?.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]).q; + return SearchUtils.buildSearchQueryJSON(query); +} + function SearchRouter() { const styles = useThemeStyles(); const {isSmallScreenWidth} = useResponsiveLayout(); const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); - const [currentQuery, setCurrentQuery] = useState(undefined); + const lastRoute = useNavigationState((state) => state.routes.at(-1)); + + // If we open SearchRouter on a `/search` page, then we prefill input with the existing Search query + const existingSearchQuery = getCurrentSearchQuery(lastRoute); + const initialQuery = existingSearchQuery ? SearchUtils.getSearchRouterInputText(existingSearchQuery) : undefined; + const initialQueryJSON = initialQuery ? SearchUtils.buildSearchQueryJSON(initialQuery) : undefined; + + const [userSearchQuery, setUserSearchQuery] = useState(initialQueryJSON); const clearUserQuery = () => { - setCurrentQuery(undefined); + setUserSearchQuery(undefined); }; const onSearchChange = debounce((userQuery: string) => { @@ -39,19 +58,38 @@ function SearchRouter() { // eslint-disable-next-line console.log('parsedQuery', queryJSON); - setCurrentQuery(queryJSON); + setUserSearchQuery(queryJSON); } else { // Handle query parsing error } }, SEARCH_DEBOUNCE_DELAY); const onSearchSubmit = useCallback(() => { + if (!userSearchQuery) { + return; + } + closeSearchRouter(); - const query = SearchUtils.buildSearchQueryString(currentQuery); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + // Because user input does not support all filters yet, we will merge user typed query with pre-existing search query + if (!existingSearchQuery) { + const query = SearchUtils.buildSearchQueryString(userSearchQuery); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + } else { + const topLevelFilters = SearchUtils.getFiltersFormValues(existingSearchQuery); + const userFilters = SearchUtils.getFiltersFormValues(userSearchQuery); + + const combinedFilters = { + ...topLevelFilters, + ...userFilters, + }; + + const query = SearchUtils.buildQueryStringFromFilterValues(combinedFilters); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + } + clearUserQuery(); - }, [currentQuery, closeSearchRouter]); + }, [closeSearchRouter, existingSearchQuery, userSearchQuery]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { closeSearchRouter(); @@ -72,6 +110,7 @@ function SearchRouter() { void; onSubmit: () => void; }; -function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputProps) { +function SearchRouterInput({initialValue = '', isFullWidth, onChange, onSubmit}: SearchRouterInputProps) { const styles = useThemeStyles(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(initialValue); const onChangeText = (text: string) => { setValue(text); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index d06be872c70a..5befa446f6f9 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -14,12 +14,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@libs/actions/Session'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList, State} from '@libs/Navigation/types'; +import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types'; import {isCentralPaneName} from '@libs/NavigationUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as SearchUtils from '@libs/SearchUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import navigationRef from '@navigation/navigationRef'; import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar'; import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton'; import variables from '@styles/variables'; @@ -113,9 +114,11 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { return; } interceptAnonymousUser(() => { - const currentSearchParams = SearchUtils.getCurrentSearchParams(); - if (currentSearchParams) { - const {q, ...rest} = currentSearchParams; + const rootState = navigationRef.getRootState() as State; + const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.CENTRAL_PANE).at(-1); + + if (lastSearchRoute) { + const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID); Navigation.navigate( diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 9760ff80ca19..d77b4e56af4d 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -64,6 +64,7 @@ const emptyPersonalDetails = { displayName: undefined, login: undefined, }; +/* Search list and results related */ /** * @private @@ -391,14 +392,6 @@ function getSortedReportActionData(data: ReportActionListItemType[]) { }); } -function getCurrentSearchParams() { - const rootState = navigationRef.getRootState() as State; - - const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.CENTRAL_PANE).at(-1); - - return lastSearchRoute ? (lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]) : undefined; -} - function isSearchResultsEmpty(searchResults: SearchResults) { return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)); } @@ -407,6 +400,8 @@ function getQueryHashFromString(query: SearchQueryString): number { return UserUtils.hashText(query, 2 ** 32); } +/* Search query related */ + /** * Update string query with all the default params that are set by parser */ @@ -781,6 +776,36 @@ function getSearchHeaderTitle( return title; } +function getSearchRouterInputText(queryJSON: SearchQueryJSON) { + const {type, status} = queryJSON; + const filters = queryJSON.flatFilters ?? {}; + + let title = `type:${type} status:${status}`; + + Object.keys(filters) + .filter((filterKey) => { + return ( + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG || + filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY + ); + }) + .forEach((key) => { + const queryFilter = filters[key as ValueOf] ?? []; + const displayQueryFilters = queryFilter.map((filter) => ({ + operator: filter.operator, + value: filter.value.toString(), + })); + title += buildFilterString(key, displayQueryFilters, ' '); + }); + + return title; +} + function buildCannedSearchQuery(type: SearchDataTypes = CONST.SEARCH.DATA_TYPES.EXPENSE, status: SearchStatus = CONST.SEARCH.STATUS.EXPENSE.ALL): SearchQueryString { return normalizeQuery(`type:${type} status:${status}`); } @@ -830,11 +855,9 @@ export { buildQueryStringFromFilterValues, buildSearchQueryJSON, buildSearchQueryString, - getCurrentSearchParams, getFiltersFormValues, getPolicyIDFromSearchQuery, getListItem, - getSearchHeaderTitle, getSections, getShouldShowMerchant, getSortedSections, @@ -842,6 +865,8 @@ export { isSearchResultsEmpty, isTransactionListItemType, isReportActionListItemType, + getSearchHeaderTitle, + getSearchRouterInputText, normalizeQuery, shouldShowYear, buildCannedSearchQuery, From c2cb9081dc3b6264b200ed6f6f22cbff58c09b24 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 18 Sep 2024 13:58:31 +0200 Subject: [PATCH 2/8] Stop merging user query with Search query after prefill --- .../Search/SearchRouter/SearchRouter.tsx | 20 +++---------------- src/libs/SearchUtils.ts | 3 --- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index d94acc2baec6..ea08f49500b3 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -71,25 +71,11 @@ function SearchRouter() { closeSearchRouter(); - // Because user input does not support all filters yet, we will merge user typed query with pre-existing search query - if (!existingSearchQuery) { - const query = SearchUtils.buildSearchQueryString(userSearchQuery); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); - } else { - const topLevelFilters = SearchUtils.getFiltersFormValues(existingSearchQuery); - const userFilters = SearchUtils.getFiltersFormValues(userSearchQuery); - - const combinedFilters = { - ...topLevelFilters, - ...userFilters, - }; - - const query = SearchUtils.buildQueryStringFromFilterValues(combinedFilters); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); - } + const query = SearchUtils.buildSearchQueryString(userSearchQuery); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); clearUserQuery(); - }, [closeSearchRouter, existingSearchQuery, userSearchQuery]); + }, [closeSearchRouter, userSearchQuery]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { closeSearchRouter(); diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index d77b4e56af4d..0afd816a0193 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -10,7 +10,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; import type * as OnyxTypes from '@src/types/onyx'; @@ -20,8 +19,6 @@ import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {translateLocal} from './Localize'; import Navigation from './Navigation/Navigation'; -import navigationRef from './Navigation/navigationRef'; -import type {AuthScreensParamList, RootStackParamList, State} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; From d11bfe1b44648142987117fda3e219a42b58d3d9 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 19 Sep 2024 16:59:33 +0200 Subject: [PATCH 3/8] Cleanup display SearchButton component on all Pages --- src/components/HeaderWithBackButton/index.tsx | 3 + src/components/HeaderWithBackButton/types.ts | 3 + src/components/MoneyReportHeader.tsx | 1 + src/components/MoneyRequestHeader.tsx | 1 + src/components/Search/SearchPageHeader.tsx | 44 +++-- .../Search/SearchRouter/SearchButton.tsx | 6 +- .../Search/SearchRouter/SearchRouter.tsx | 21 +-- .../Search/SearchRouter/SearchRouterInput.tsx | 5 +- .../createCustomBottomTabNavigator/TopBar.tsx | 7 +- src/libs/SearchUtils.ts | 166 +++++++----------- src/pages/AddressPage.tsx | 1 + src/pages/Search/AdvancedSearchFilters.tsx | 2 +- src/pages/Search/SearchPageBottomTab.tsx | 1 + src/pages/Search/SearchTypeMenuNarrow.tsx | 2 +- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 1 + src/pages/home/HeaderView.tsx | 2 + src/pages/settings/AboutPage/AboutPage.tsx | 1 + .../settings/Preferences/PreferencesPage.tsx | 1 + src/pages/settings/Profile/ProfilePage.tsx | 1 + .../Security/SecuritySettingsPage.tsx | 1 + .../Subscription/CardSection/CardSection.tsx | 2 +- .../Troubleshoot/TroubleshootPage.tsx | 1 + .../settings/Wallet/WalletPage/WalletPage.tsx | 1 + src/pages/workspace/WorkspacesListPage.tsx | 2 + 24 files changed, 126 insertions(+), 150 deletions(-) diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index f1e715bface8..eb04ad5540eb 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -7,6 +7,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PinButton from '@components/PinButton'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import SearchButton from '@components/Search/SearchRouter/SearchButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -60,6 +61,7 @@ function HeaderWithBackButton({ shouldOverlayDots = false, shouldOverlay = false, shouldNavigateToTopMostReport = false, + shouldDisplaySearchRouter = false, progressBarPercentage, style, }: HeaderWithBackButtonProps) { @@ -261,6 +263,7 @@ function HeaderWithBackButton({ )} + {shouldDisplaySearchRouter && } diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index c55a7bddc80c..7423abcfe3b0 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -128,6 +128,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should overlay the 3 dots menu */ shouldOverlayDots?: boolean; + /** Whether we should display button that opens new SearchRouter */ + shouldDisplaySearchRouter?: boolean; + /** 0 - 100 number indicating current progress of the progress bar */ progressBarPercentage?: number; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4fc92d619e68..f5e2703b1a47 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -285,6 +285,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea report={moneyRequestReport} policy={policy} shouldShowBackButton={shouldUseNarrowLayout} + shouldDisplaySearchRouter onBackButtonPress={onBackButtonPress} // Shows border if no buttons or banners are showing below the header shouldShowBorderBottom={!isMoreContentShown} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 0e0633042a7d..34d3c2d54a70 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -134,6 +134,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow policy={policy} shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={onBackButtonPress} + shouldDisplaySearchRouter > {hasAllPendingRTERViolations && !shouldUseNarrowLayout && (