diff --git a/src/CONST.ts b/src/CONST.ts index 175aa8cd3c16..42b33d5671c8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5282,6 +5282,7 @@ const CONST = { STATUS: 'status', SORT_BY: 'sortBy', SORT_ORDER: 'sortOrder', + POLICY_ID: 'policyID', }, SYNTAX_FILTER_KEYS: { DATE: 'date', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2db861121375..ff27c1434cde 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -35,8 +35,7 @@ const ROUTES = { SEARCH_CENTRAL_PANE: { route: 'search', - getRoute: ({query, isCustomQuery = false, policyIDs}: {query: SearchQueryString; isCustomQuery?: boolean; policyIDs?: string}) => - `search?q=${query}&isCustomQuery=${isCustomQuery}${policyIDs ? `&policyIDs=${policyIDs}` : ''}` as const, + getRoute: ({query, isCustomQuery = false}: {query: SearchQueryString; isCustomQuery?: boolean}) => `search?q=${query}&isCustomQuery=${isCustomQuery}` as const, }, SEARCH_ADVANCED_FILTERS: 'search/filters', SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date', diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index db729a9aa77d..dd175023a52c 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -37,7 +37,6 @@ import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactio type SearchProps = { queryJSON: SearchQueryJSON; isCustomQuery: boolean; - policyIDs?: string; }; const transactionItemMobileHeight = 100; @@ -74,7 +73,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact return {...selectedTransactions, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}}; } -function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { +function Search({queryJSON, isCustomQuery}: SearchProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -118,7 +117,7 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { return; } - SearchActions.search({queryJSON, offset, policyIDs}); + SearchActions.search({queryJSON, offset}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOffline, offset, queryJSON]); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index c933deb8ee03..9f2aca1ff957 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -63,6 +63,7 @@ type SearchQueryAST = { sortBy: SearchColumnType; sortOrder: SortOrder; filters: ASTNode; + policyID?: string; }; type SearchQueryJSON = { diff --git a/src/hooks/useActiveWorkspaceFromNavigationState.ts b/src/hooks/useActiveWorkspaceFromNavigationState.ts index db7d13a00aaa..d2851e83ab6c 100644 --- a/src/hooks/useActiveWorkspaceFromNavigationState.ts +++ b/src/hooks/useActiveWorkspaceFromNavigationState.ts @@ -12,22 +12,20 @@ import SCREENS from '@src/SCREENS'; */ function useActiveWorkspaceFromNavigationState() { // The last policyID value is always stored in the last route in BottomTabNavigator. - const activeWorkpsaceID = useNavigationState((state) => { + const activeWorkspaceID = useNavigationState((state) => { // SCREENS.HOME is a screen located in the BottomTabNavigator, if it's not in state.routeNames it means that this hook was called from a screen in another navigator. if (!state.routeNames.includes(SCREENS.HOME)) { Log.warn('useActiveWorkspaceFromNavigationState should be called only from BottomTab screens'); } - const policyID = state.routes.at(-1)?.params?.policyID; + const params = state.routes.at(-1)?.params ?? {}; - if (!policyID) { - return undefined; + if ('policyID' in params) { + return params?.policyID; } - - return policyID; }); - return activeWorkpsaceID; + return activeWorkspaceID; } export default useActiveWorkspaceFromNavigationState; diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts index 530388dc7f47..64bfc5baf5a1 100644 --- a/src/libs/API/parameters/Search.ts +++ b/src/libs/API/parameters/Search.ts @@ -3,8 +3,6 @@ import type {SearchQueryString} from '@components/Search/types'; type SearchParams = { hash: number; jsonQuery: SearchQueryString; - // Tod this is temporary, remove top level policyIDs as part of: https://github.com/Expensify/App/issues/46592 - policyIDs?: string; }; export default SearchParams; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index a9df43ad5b64..394a617278d4 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -5,6 +5,7 @@ import {useOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; +import type {SearchQueryString} from '@components/Search/types'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; @@ -36,6 +37,34 @@ type BottomTabBarProps = { selectedTab: string | undefined; }; +/** + * Returns SearchQueryString that has policyID correctly set. + * + * When we're coming back to Search Screen we might have pre-existing policyID inside SearchQuery. + * There are 2 cases when we might want to remove this `policyID`: + * - if Policy was removed in another screen + * - if WorkspaceSwitcher was used to globally unset a policyID + * Otherwise policyID will be inserted into query + */ +function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: string): SearchQueryString { + const queryJSON = SearchUtils.buildSearchQueryJSON(query); + if (!queryJSON) { + return query; + } + + const policyID = queryJSON.policyID ?? activePolicyID; + const policy = PolicyUtils.getPolicy(policyID); + + // In case policy is missing or there is no policy currently selected via WorkspaceSwitcher we remove it + if (!activePolicyID || !policy) { + delete queryJSON.policyID; + } else { + queryJSON.policyID = policyID; + } + + return SearchUtils.buildSearchQueryString(queryJSON); +} + function BottomTabBar({selectedTab}: BottomTabBarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -91,13 +120,23 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { const currentSearchParams = SearchUtils.getCurrentSearchParams(); if (currentSearchParams) { const {q, ...rest} = currentSearchParams; - const policy = PolicyUtils.getPolicy(currentSearchParams?.policyIDs); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: q, ...rest, policyIDs: policy ? currentSearchParams?.policyIDs : undefined})); + const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID); + + Navigation.navigate( + ROUTES.SEARCH_CENTRAL_PANE.getRoute({ + query: cleanedQuery, + ...rest, + }), + ); return; } - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()})); + + const defaultCannedQuery = SearchUtils.buildCannedSearchQuery(); + // when navigating to search we might have an activePolicyID set from workspace switcher + const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery; + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); }); - }, [selectedTab]); + }, [activeWorkspaceID, selectedTab]); return ( diff --git a/src/libs/Navigation/extractPolicyIDFromQuery.ts b/src/libs/Navigation/extractPolicyIDFromQuery.ts new file mode 100644 index 000000000000..bd0464f4aab6 --- /dev/null +++ b/src/libs/Navigation/extractPolicyIDFromQuery.ts @@ -0,0 +1,22 @@ +import * as SearchUtils from '@libs/SearchUtils'; +import type {NavigationPartialRoute} from './types'; + +function extractPolicyIDFromQuery(route?: NavigationPartialRoute) { + if (!route?.params) { + return undefined; + } + + if (!('q' in route.params)) { + return undefined; + } + + const queryString = route.params.q as string; + const queryJSON = SearchUtils.buildSearchQueryJSON(queryString); + if (!queryJSON) { + return undefined; + } + + return SearchUtils.getPolicyIDFromSearchQuery(queryJSON); +} + +export default extractPolicyIDFromQuery; diff --git a/src/libs/Navigation/getPolicyIDFromState.ts b/src/libs/Navigation/getPolicyIDFromState.ts index 00236fb0fce0..62690f29a98f 100644 --- a/src/libs/Navigation/getPolicyIDFromState.ts +++ b/src/libs/Navigation/getPolicyIDFromState.ts @@ -1,16 +1,23 @@ +import extractPolicyIDFromQuery from './extractPolicyIDFromQuery'; import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; import type {RootStackParamList, State} from './types'; +/** + * returns policyID value if one exists in navigation state + * + * PolicyID in this app can be stored in two ways: + * - on most screens but NOT Search as `policyID` param + * - on Search related screens as policyID filter inside `q` (SearchQuery) param + */ const getPolicyIDFromState = (state: State): string | undefined => { const topmostBottomTabRoute = getTopmostBottomTabRoute(state); - const shouldAddPolicyIDToUrl = !!topmostBottomTabRoute && !!topmostBottomTabRoute.params && 'policyID' in topmostBottomTabRoute.params && !!topmostBottomTabRoute.params?.policyID; - - if (!shouldAddPolicyIDToUrl) { - return undefined; + const policyID = topmostBottomTabRoute && topmostBottomTabRoute.params && 'policyID' in topmostBottomTabRoute.params && topmostBottomTabRoute.params?.policyID; + if (policyID) { + return topmostBottomTabRoute.params?.policyID as string; } - return topmostBottomTabRoute.params?.policyID as string; + return extractPolicyIDFromQuery(topmostBottomTabRoute); }; export default getPolicyIDFromState; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 45178ef1adaf..1fc99c771ca5 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -4,13 +4,13 @@ import {findFocusedRoute} from '@react-navigation/native'; import {omitBy} from 'lodash'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP'; -import extractPolicyIDsFromState from '@libs/Navigation/linkingConfig/extractPolicyIDsFromState'; import {isCentralPaneName} from '@libs/NavigationUtils'; import shallowCompare from '@libs/ObjectUtils'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import getActionsFromPartialDiff from '@navigation/AppNavigator/getActionsFromPartialDiff'; import getPartialStateDiff from '@navigation/AppNavigator/getPartialStateDiff'; import dismissModal from '@navigation/dismissModal'; +import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery'; import extrapolateStateFromParams from '@navigation/extrapolateStateFromParams'; import getPolicyIDFromState from '@navigation/getPolicyIDFromState'; import getStateFromPath from '@navigation/getStateFromPath'; @@ -49,18 +49,18 @@ export default function linkTo(navigation: NavigationContainerRef>; // Creating path with /w/ included if necessary. const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); - const policyIDs = !!topmostCentralPaneRoute?.params && 'policyIDs' in topmostCentralPaneRoute.params ? (topmostCentralPaneRoute?.params?.policyIDs as string) : ''; + const extractedPolicyID = extractPolicyIDFromPath(`/${path}`); const policyIDFromState = getPolicyIDFromState(rootState); - const policyID = extractedPolicyID ?? policyIDFromState ?? policyIDs; + const policyID = extractedPolicyID ?? policyIDFromState; const lastRoute = rootState?.routes?.at(-1); const isNarrowLayout = getIsNarrowLayout(); const isFullScreenOnTop = lastRoute?.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR; - // policyIDs is present only on SCREENS.SEARCH.CENTRAL_PANE and it's displayed in the url as a query param, on the other pages this parameter is called policyID and it's shown in the url in the format: /w/:policyID - if (policyID && !isFullScreenOnTop && !policyIDs) { + // policyID on SCREENS.SEARCH.CENTRAL_PANE can be present only as part of SearchQuery, while on other pages it's stored in the url in the format: /w/:policyID/ + if (policyID && !isFullScreenOnTop && !policyIDFromState) { // The stateFromPath doesn't include proper path if there is a policy passed with /w/id. // We need to replace the path in the state with the proper one. // To avoid this hacky solution we may want to create custom getActionFromState function in the future. @@ -95,8 +95,10 @@ export default function linkTo(navigation: NavigationContainerRef)?.policyID ?? '') !== @@ -115,11 +117,6 @@ export default function linkTo(navigation: NavigationContainerRef).policyIDs = policyID; - } - // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow // and at the same time we want the back button to go to the page we were before the deeplink } else if (type === CONST.NAVIGATION.TYPE.UP) { diff --git a/src/libs/Navigation/linkingConfig/extractPolicyIDsFromState.ts b/src/libs/Navigation/linkingConfig/extractPolicyIDsFromState.ts deleted file mode 100644 index fdaf7e6eb490..000000000000 --- a/src/libs/Navigation/linkingConfig/extractPolicyIDsFromState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {InitialState} from '@react-navigation/native'; -import {findFocusedRoute} from '@react-navigation/native'; -import SCREENS from '@src/SCREENS'; - -function extractPolicyIDsFromState(state: InitialState) { - const focusedRoute = findFocusedRoute(state); - if (focusedRoute && focusedRoute.name === SCREENS.SEARCH.CENTRAL_PANE && focusedRoute.params && 'policyIDs' in focusedRoute.params) { - return focusedRoute.params.policyIDs as string; - } - return undefined; -} - -export default extractPolicyIDsFromState; diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 611831544bdc..10e68ad4a6a8 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -8,6 +8,7 @@ import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRo import {isCentralPaneName} from '@libs/NavigationUtils'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import * as ReportConnection from '@libs/ReportConnection'; +import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -15,7 +16,6 @@ import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config, {normalizedConfigs} from './config'; -import extractPolicyIDsFromState from './extractPolicyIDsFromState'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForState'; import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState'; @@ -379,10 +379,11 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { throw new Error('Unable to parse path'); } - // Only on SCREENS.SEARCH.CENTRAL_PANE policyID is stored differently as "policyIDs" param, so we're handling this case here - const policyIDs = extractPolicyIDsFromState(state); + // On SCREENS.SEARCH.CENTRAL_PANE policyID is stored differently inside search query ("q" param), so we're handling this case + const focusedRoute = findFocusedRoute(state); + const policyIDFromQuery = extractPolicyIDFromQuery(focusedRoute); - return getAdaptedState(state, policyID ?? policyIDs); + return getAdaptedState(state, policyID ?? policyIDFromQuery); }; export default getAdaptedStateFromPath; diff --git a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts index 67d76de4932d..7b213fdfeb6e 100644 --- a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts +++ b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts @@ -24,12 +24,9 @@ function getMatchingBottomTabRouteForState(state: State, pol if (tabName === SCREENS.SEARCH.BOTTOM_TAB) { const topmostCentralPaneRouteParams = {...topmostCentralPaneRoute.params} as Record; - delete topmostCentralPaneRouteParams?.policyIDs; - if (policyID) { - topmostCentralPaneRouteParams.policyID = policyID; - } return {name: tabName, params: topmostCentralPaneRouteParams}; } + return {name: tabName, params: paramsWithPolicyID}; } diff --git a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts index 107dae2bb74c..f7c2140b1117 100644 --- a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts +++ b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts @@ -17,7 +17,7 @@ function setupCustomAndroidBackHandler() { const bottomTabRoutes = bottomTabRoute?.state?.routes; const focusedRoute = findFocusedRoute(rootState); - // Shoudn't happen but for type safety. + // Shouldn't happen but for type safety. if (!bottomTabRoutes) { return false; } @@ -38,15 +38,15 @@ function setupCustomAndroidBackHandler() { const bottomTabRouteAfterPop = bottomTabRoutes.at(-2); // It's possible that central pane search is desynchronized with the bottom tab search. - // e.g. opening a tab different than search will wipe out central pane screens. + // e.g. opening a tab different from search will wipe out central pane screens. // In that case we have to push the proper one. if ( bottomTabRouteAfterPop && bottomTabRouteAfterPop.name === SCREENS.SEARCH.BOTTOM_TAB && (!centralPaneRouteAfterPop || centralPaneRouteAfterPop.name !== SCREENS.SEARCH.CENTRAL_PANE) ) { - const {policyID, ...restParams} = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; - navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, {...restParams, policyIDs: policyID})}); + const searchParams = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; + navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, searchParams)}); } return true; @@ -54,11 +54,11 @@ function setupCustomAndroidBackHandler() { // Handle back press to go back to the search page. // It's possible that central pane search is desynchronized with the bottom tab search. - // e.g. opening a tab different than search will wipe out central pane screens. + // e.g. opening a tab different from search will wipe out central pane screens. // In that case we have to push the proper one. if (bottomTabRoutes && bottomTabRoutes?.length >= 2 && bottomTabRoutes[bottomTabRoutes.length - 2].name === SCREENS.SEARCH.BOTTOM_TAB && rootState?.routes?.length === 1) { - const {policyID, ...restParams} = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; - navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, {...restParams, policyIDs: policyID})}); + const searchParams = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; + navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, searchParams)}); navigationRef.dispatch({...StackActions.pop(), target: bottomTabRoute?.state?.key}); return true; } diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index a37ccb0c2506..f65b32006c0e 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -20,7 +20,7 @@ type ActionPayloadParams = { path?: string; }; -type CentralPaneRouteParams = Record & {policyID?: string; policyIDs?: string; reportID?: string}; +type CentralPaneRouteParams = Record & {policyID?: string; q?: string; reportID?: string}; function checkIfActionPayloadNameIsEqual(action: Writable, screenName: string) { return action?.payload && 'name' in action.payload && action?.payload?.name === screenName; @@ -109,12 +109,19 @@ export default function switchPolicyID(navigation: NavigationContainerRef & {policyID: string}; + [SCREENS.SEARCH.BOTTOM_TAB]: CentralPaneScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; [SCREENS.SETTINGS.ROOT]: {policyID?: string}; }; diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 64d5334aa265..403e8442ed5b 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -199,7 +199,8 @@ function peg$parse(input, options) { var peg$c21 = "keyword"; var peg$c22 = "sortBy"; var peg$c23 = "sortOrder"; - var peg$c24 = "\""; + var peg$c24 = "policyID"; + var peg$c25 = "\""; var peg$r0 = /^[:=]/; var peg$r1 = /^[^"\r\n]/; @@ -231,13 +232,21 @@ function peg$parse(input, options) { var peg$e22 = peg$literalExpectation("keyword", false); var peg$e23 = peg$literalExpectation("sortBy", false); var peg$e24 = peg$literalExpectation("sortOrder", false); - var peg$e25 = peg$literalExpectation("\"", false); - var peg$e26 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e27 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); - var peg$e28 = peg$otherExpectation("whitespace"); - var peg$e29 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e25 = peg$literalExpectation("policyID", false); + var peg$e26 = peg$literalExpectation("\"", false); + var peg$e27 = peg$classExpectation(["\"", "\r", "\n"], true, false); + var peg$e28 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); + var peg$e29 = peg$otherExpectation("whitespace"); + var peg$e30 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + + var peg$f0 = function(filters) { + const withDefaults = applyDefaults(filters); + if (defaultValues.policyID) { + return applyPolicyID(withDefaults); + } - var peg$f0 = function(filters) { return applyDefaults(filters); }; + return withDefaults; + }; var peg$f1 = function(head, tail) { const allFilters = [head, ...tail.map(([_, filter]) => filter)].filter(filter => filter !== null); if (!allFilters.length) { @@ -251,10 +260,9 @@ function peg$parse(input, options) { if(!keywords.length){ return nonKeywords.reduce((result, filter) => buildFilter("and", result, filter)) } - + return buildFilter("and", keywords.reduce((result, filter) => buildFilter("or", result, filter)), nonKeywords.reduce((result, filter) => buildFilter("and", result, filter))) - - + return allFilters.reduce((result, filter) => buildFilter("and", result, filter)); }; var peg$f2 = function(field, op, value) { @@ -263,6 +271,11 @@ function peg$parse(input, options) { return null; } + if (isPolicyID(field)) { + updateDefaultValues(field, value.trim()); + return null; + } + if (!field && !op) { return buildFilter('eq', 'keyword', value.trim()); } @@ -297,10 +310,11 @@ function peg$parse(input, options) { var peg$f25 = function() { return "keyword"; }; var peg$f26 = function() { return "sortBy"; }; var peg$f27 = function() { return "sortOrder"; }; - var peg$f28 = function(parts) { return parts.join(''); }; - var peg$f29 = function(chars) { return chars.join(''); }; + var peg$f28 = function() { return "policyID"; }; + var peg$f29 = function(parts) { return parts.join(''); }; var peg$f30 = function(chars) { return chars.join(''); }; - var peg$f31 = function() { return "and"; }; + var peg$f31 = function(chars) { return chars.join(''); }; + var peg$f32 = function() { return "and"; }; var peg$currPos = options.peg$currPos | 0; var peg$savedPos = peg$currPos; var peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -909,6 +923,21 @@ function peg$parse(input, options) { s1 = peg$f27(); } s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 8) === peg$c24) { + s1 = peg$c24; + peg$currPos += 8; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e25); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f28(); + } + s0 = s1; + } } } } @@ -953,7 +982,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f28(s1); + s1 = peg$f29(s1); } s0 = s1; @@ -965,11 +994,11 @@ function peg$parse(input, options) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c24; + s1 = peg$c25; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e25); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } } if (s1 !== peg$FAILED) { s2 = []; @@ -978,7 +1007,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } + if (peg$silentFails === 0) { peg$fail(peg$e27); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -987,19 +1016,19 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } + if (peg$silentFails === 0) { peg$fail(peg$e27); } } } if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c24; + s3 = peg$c25; peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e25); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f29(s2); + s0 = peg$f30(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1022,7 +1051,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } + if (peg$silentFails === 0) { peg$fail(peg$e28); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -1032,7 +1061,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } + if (peg$silentFails === 0) { peg$fail(peg$e28); } } } } else { @@ -1040,7 +1069,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f30(s1); + s1 = peg$f31(s1); } s0 = s1; @@ -1053,7 +1082,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f31(); + s1 = peg$f32(); s0 = s1; return s0; @@ -1069,7 +1098,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e30); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -1078,12 +1107,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e30); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e29); } return s0; } @@ -1106,7 +1135,14 @@ function peg$parse(input, options) { filters }; } - + + function applyPolicyID(filtersWithDefaults) { + return { + ...filtersWithDefaults, + policyID: filtersWithDefaults.policyID + }; + } + function updateDefaultValues(field, value) { defaultValues[field] = value; } @@ -1115,6 +1151,10 @@ function peg$parse(input, options) { return defaultValues.hasOwnProperty(field); } + function isPolicyID(field) { + return field === 'policyID'; + } + peg$result = peg$startRuleFunction(); if (options.peg$library) { diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index d9ede101f7f8..2f3efb39a6b1 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -32,7 +32,14 @@ filters }; } - + + function applyPolicyID(filtersWithDefaults) { + return { + ...filtersWithDefaults, + policyID: filtersWithDefaults.policyID + }; + } + function updateDefaultValues(field, value) { defaultValues[field] = value; } @@ -40,10 +47,21 @@ function isDefaultField(field) { return defaultValues.hasOwnProperty(field); } + + function isPolicyID(field) { + return field === 'policyID'; + } } query - = _ filters:filterList? _ { return applyDefaults(filters); } + = _ filters:filterList? _ { + const withDefaults = applyDefaults(filters); + if (defaultValues.policyID) { + return applyPolicyID(withDefaults); + } + + return withDefaults; + } filterList = head:filter tail:(logicalAnd filter)* { @@ -59,10 +77,9 @@ filterList if(!keywords.length){ return nonKeywords.reduce((result, filter) => buildFilter("and", result, filter)) } - + return buildFilter("and", keywords.reduce((result, filter) => buildFilter("or", result, filter)), nonKeywords.reduce((result, filter) => buildFilter("and", result, filter))) - - + return allFilters.reduce((result, filter) => buildFilter("and", result, filter)); } @@ -73,6 +90,11 @@ filter return null; } + if (isPolicyID(field)) { + updateDefaultValues(field, value.trim()); + return null; + } + if (!field && !op) { return buildFilter('eq', 'keyword', value.trim()); } @@ -111,6 +133,7 @@ key / "keyword" { return "keyword"; } / "sortBy" { return "sortBy"; } / "sortOrder" { return "sortOrder"; } + / "policyID" { return "policyID"; } identifier = parts:(quotedString / alphanumeric)+ { return parts.join(''); } diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 8d4a59f3987c..f5656a5967d1 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -305,9 +305,7 @@ function getCurrentSearchParams() { } if (lastSearchBottomTabRoute) { - const {policyID, ...rest} = lastSearchBottomTabRoute.params as BottomTabNavigatorParamList[typeof SCREENS.SEARCH.BOTTOM_TAB]; - const params: AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE] = {policyIDs: policyID, ...rest}; - return params; + return lastSearchBottomTabRoute.params as BottomTabNavigatorParamList[typeof SCREENS.SEARCH.BOTTOM_TAB]; } } @@ -319,18 +317,16 @@ function getQueryHashFromString(query: SearchQueryString): number { return UserUtils.hashText(query, 2 ** 32); } -function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) { +function buildSearchQueryJSON(query: SearchQueryString) { try { - // Add the full input and hash to the results const result = searchParser.parse(query) as SearchQueryJSON; - result.inputQuery = query; - // Temporary solution until we move policyID filter into the AST - then remove this line and keep only query - const policyIDPart = policyID ?? ''; - result.hash = getQueryHashFromString(query + policyIDPart); + // Add the full input and hash to the results + result.inputQuery = query; + result.hash = getQueryHashFromString(query); return result; } catch (e) { - console.error(e); + console.error(`Error when parsing SearchQuery: "${query}"`, e); } } @@ -338,12 +334,12 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { const queryParts: string[] = []; const defaultQueryJSON = buildSearchQueryJSON(''); - // For this const values are lowercase version of the keys. We are using lowercase for ast keys. for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) { - if (queryJSON?.[key]) { - queryParts.push(`${key}:${queryJSON[key]}`); - } else if (defaultQueryJSON) { - queryParts.push(`${key}:${defaultQueryJSON[key]}`); + const existingFieldValue = queryJSON?.[key]; + const queryFieldValue = existingFieldValue ?? defaultQueryJSON?.[key]; + + if (queryFieldValue) { + queryParts.push(`${key}:${queryFieldValue}`); } } @@ -520,6 +516,26 @@ function getFilters(queryJSON: SearchQueryJSON) { return filters; } +/** + * Given a SearchQueryJSON this function will try to find the value of policyID filter saved in query + * and return just the first policyID value from the filter. + * + * Note: `policyID` property can store multiple policy ids (just like many other search filters) as a comma separated value; + * however there are several places in the app (related to WorkspaceSwitcher) that will accept only a single policyID. + */ +function getPolicyIDFromSearchQuery(queryJSON: SearchQueryJSON) { + const policyIDFilter = queryJSON.policyID; + + if (!policyIDFilter) { + return; + } + + // policyID is a comma-separated value + const [policyID] = policyIDFilter.split(','); + + return policyID; +} + function buildFilterString(filterName: string, queryFilters: QueryFilter[]) { let filterValueString = ''; queryFilters.forEach((queryFilter, index) => { @@ -558,6 +574,7 @@ export { buildSearchQueryString, getCurrentSearchParams, getFilters, + getPolicyIDFromSearchQuery, getListItem, getQueryHash, getSearchHeaderTitle, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b51955c9cc59..020fc1bd2c30 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -49,7 +49,7 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall return {optimisticData, finallyData}; } -function search({queryJSON, offset, policyIDs}: {queryJSON: SearchQueryJSON; offset?: number; policyIDs?: string}) { +function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) { const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash); const queryWithOffset = { @@ -58,7 +58,7 @@ function search({queryJSON, offset, policyIDs}: {queryJSON: SearchQueryJSON; off }; const jsonQuery = JSON.stringify(queryWithOffset); - API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery, policyIDs}, {optimisticData, finallyData}); + API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery}, {optimisticData, finallyData}); } /** diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index d5725c2d40a2..37735cddce3f 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -16,9 +16,9 @@ type SearchPageProps = StackScreenProps SearchUtils.buildSearchQueryJSON(q, policyIDs), [q, policyIDs]); + const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(q), [q]); const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()})); // On small screens this page is not displayed, the configuration is in the file: src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx @@ -43,7 +43,6 @@ function SearchPage({route}: SearchPageProps) { )} diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 029407026dc8..8b03da81456e 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -27,17 +27,17 @@ function SearchPageBottomTab() { const {clearSelectedTransactions} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); - const {queryJSON, policyIDs, isCustomQuery} = useMemo(() => { - if (!activeCentralPaneRoute || activeCentralPaneRoute.name !== SCREENS.SEARCH.CENTRAL_PANE) { - return {queryJSON: undefined, policyIDs: undefined}; + const {queryJSON, policyID, isCustomQuery} = useMemo(() => { + if (activeCentralPaneRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE) { + return {queryJSON: undefined, policyID: undefined, isCustomQuery: undefined}; } - // This will be SEARCH_CENTRAL_PANE as we checked that in if. - const searchParams = activeCentralPaneRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; + const searchParams = activeCentralPaneRoute?.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; + const parsedQuery = SearchUtils.buildSearchQueryJSON(searchParams?.q); return { - queryJSON: SearchUtils.buildSearchQueryJSON(searchParams.q, searchParams.policyIDs), - policyIDs: searchParams.policyIDs, + queryJSON: parsedQuery, + policyID: parsedQuery && SearchUtils.getPolicyIDFromSearchQuery(parsedQuery), isCustomQuery: searchParams.isCustomQuery, }; }, [activeCentralPaneRoute]); @@ -58,7 +58,7 @@ function SearchPageBottomTab() { {!selectionMode?.isEnabled && queryJSON ? ( <> @@ -80,7 +80,6 @@ function SearchPageBottomTab() { )}