diff --git a/src/App.tsx b/src/App.tsx index cf0fd5528eec..35254fa29b2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; +import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; @@ -94,6 +95,7 @@ function App({url}: AppProps) { VolumeContextProvider, VideoPopoverMenuContextProvider, KeyboardProvider, + SearchRouterContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 86cbd4c28fc9..7ea2af2b1522 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1054,7 +1054,6 @@ const CONST = { BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', - ONBOARDING: 'onboarding', }, ANCHOR_ORIGIN_VERTICAL: { TOP: 'top', diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx new file mode 100644 index 000000000000..4948c90ce3d1 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Permissions from '@libs/Permissions'; +import {useSearchRouterContext} from './SearchRouterContext'; + +function SearchButton() { + const styles = useThemeStyles(); + const theme = useTheme(); + const {openSearchRouter} = useSearchRouterContext(); + + if (!Permissions.canUseNewSearchRouter()) { + return; + } + + return ( + { + openSearchRouter(); + }} + > + + + ); +} + +SearchButton.displayName = 'SearchButton'; + +export default SearchButton; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx new file mode 100644 index 000000000000..b38c55279c1e --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -0,0 +1,102 @@ +import debounce from 'lodash/debounce'; +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import Modal from '@components/Modal'; +import type {SearchQueryJSON} from '@components/Search/types'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as SearchUtils from '@libs/SearchUtils'; +import Navigation from '@navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import {useSearchRouterContext} from './SearchRouterContext'; +import SearchRouterInput from './SearchRouterInput'; + +const SEARCH_DEBOUNCE_DELAY = 200; + +function SearchRouter() { + const styles = useThemeStyles(); + + const {isSmallScreenWidth} = useResponsiveLayout(); + const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); + const [currentQuery, setCurrentQuery] = useState(undefined); + + const clearUserQuery = () => { + setCurrentQuery(undefined); + }; + + const onSearchChange = debounce((userQuery: string) => { + if (!userQuery) { + clearUserQuery(); + return; + } + + const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery); + + if (queryJSON) { + // eslint-disable-next-line + console.log('parsedQuery', queryJSON); + + setCurrentQuery(queryJSON); + } else { + // Handle query parsing error + } + }, SEARCH_DEBOUNCE_DELAY); + + const onSearchSubmit = useCallback(() => { + closeSearchRouter(); + + const query = SearchUtils.buildSearchQueryString(currentQuery); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + clearUserQuery(); + }, [currentQuery, closeSearchRouter]); + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.ENTER, + () => { + if (!currentQuery) { + return; + } + + onSearchSubmit(); + }, + { + captureOnInputs: true, + shouldBubble: false, + }, + ); + + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { + closeSearchRouter(); + clearUserQuery(); + }); + + const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; + const isFullWidth = isSmallScreenWidth; + + return ( + + + + + + + + ); +} + +SearchRouter.displayName = 'SearchRouter'; + +export default SearchRouter; diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx new file mode 100644 index 000000000000..d935fff110a4 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -0,0 +1,37 @@ +import React, {useContext, useMemo, useState} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +const defaultSearchContext = { + isSearchRouterDisplayed: false, + openSearchRouter: () => {}, + closeSearchRouter: () => {}, +}; + +type SearchRouterContext = typeof defaultSearchContext; + +const Context = React.createContext(defaultSearchContext); + +function SearchRouterContextProvider({children}: ChildrenProps) { + const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false); + + const routerContext = useMemo(() => { + const openSearchRouter = () => setIsSearchRouterDisplayed(true); + const closeSearchRouter = () => setIsSearchRouterDisplayed(false); + + return { + isSearchRouterDisplayed, + openSearchRouter, + closeSearchRouter, + }; + }, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]); + + return {children}; +} + +function useSearchRouterContext() { + return useContext(Context); +} + +SearchRouterContextProvider.displayName = 'SearchRouterContextProvider'; + +export {SearchRouterContextProvider, useSearchRouterContext}; diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx new file mode 100644 index 000000000000..860a46239d21 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -0,0 +1,41 @@ +import React, {useState} from 'react'; +import BaseTextInput from '@components/TextInput/BaseTextInput'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +type SearchRouterInputProps = { + isFullWidth: boolean; + onChange: (searchTerm: string) => void; + onSubmit: () => void; +}; + +function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputProps) { + const styles = useThemeStyles(); + + const [value, setValue] = useState(''); + + const onChangeText = (text: string) => { + setValue(text); + onChange(text); + }; + + const modalWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; + + return ( + + ); +} + +SearchRouterInput.displayName = 'SearchRouterInput'; + +export default SearchRouterInput; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f2461f400678..8f1b27baa888 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -558,6 +559,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie ); })} + ); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index f4728a80f2ca..985c16d50c22 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -5,6 +5,7 @@ import Breadcrumbs from '@components/Breadcrumbs'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; +import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; @@ -73,6 +74,8 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {translate('common.cancel')} )} + {/* This is only temporary for development and will be cleaned up in: https://github.com/Expensify/App/issues/49122 */} + {displaySearch && ( ): boolean { return !!betas?.includes(CONST.BETAS.ALL); @@ -49,6 +50,17 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.COMBINED_TRACK_SUBMIT); } +/** + * New Search Router is under construction and for now should be displayed only in dev to allow developers to work on it. + * We are not using BETA for this feature, as betas are heavier to cleanup, + * and the development of new router is expected to take 2-3 weeks at most + * + * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. + */ +function canUseNewSearchRouter() { + return Environment.isDevelopment(); +} + /** * Link previews are temporarily disabled. */ @@ -68,4 +80,5 @@ export default { canUseNewDotCopilot, canUseWorkspaceRules, canUseCombinedTrackSubmit, + canUseNewSearchRouter, }; diff --git a/src/styles/index.ts b/src/styles/index.ts index 9dfd18b641e8..44362ab75ded 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3593,31 +3593,19 @@ const styles = (theme: ThemeColors) => flex: 1, }, - searchPressable: { - height: variables.componentSizeNormal, - }, - - searchContainer: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - gap: 8, - paddingHorizontal: 24, - backgroundColor: theme.hoverComponentBG, - borderRadius: variables.componentBorderRadiusRounded, - justifyContent: 'center', - }, - - searchContainerHovered: { - backgroundColor: theme.border, - }, - searchInputStyle: { color: theme.textSupporting, fontSize: 13, lineHeight: 16, }, + searchRouterInputStyle: { + borderRadius: variables.componentBorderRadiusSmall, + borderWidth: 2, + borderColor: theme.borderFocus, + paddingHorizontal: 8, + }, + searchTableHeaderActive: { fontWeight: FontUtils.fontWeight.bold, },