diff --git a/src/CONST.ts b/src/CONST.ts index c7a6741da314..d6fd867c6781 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5233,12 +5233,20 @@ const CONST = { ASC: 'asc', DESC: 'desc', }, - TAB: { + STATUS: { ALL: 'all', SHARED: 'shared', DRAFTS: 'drafts', FINISHED: 'finished', }, + TAB: { + EXPENSE: { + ALL: 'type:expense status:all', + SHARED: 'type:expense status:shared', + DRAFTS: 'type:expense status:drafts', + FINISHED: 'type:expense status:finished', + }, + }, TABLE_COLUMNS: { RECEIPT: 'receipt', DATE: 'date', @@ -5268,7 +5276,6 @@ const CONST = { STATUS: 'status', SORT_BY: 'sortBy', SORT_ORDER: 'sortOrder', - OFFSET: 'offset', }, SYNTAX_FILTER_KEYS: { DATE: 'date', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 827e0f3b748d..a288af6c6fec 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,10 +1,9 @@ import type {TupleToUnion, ValueOf} from 'type-fest'; +import type {SearchQueryString} from './components/Search/types'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; -import type {AuthScreensParamList} from './libs/Navigation/types'; import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy'; -import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; // This is a file containing constants for all the routes we want to be able to go to @@ -37,16 +36,9 @@ const ROUTES = { ALL_SETTINGS: 'all-settings', SEARCH_CENTRAL_PANE: { - route: '/search/:query', - getRoute: (searchQuery: SearchQuery, queryParams?: AuthScreensParamList['Search_Central_Pane']) => { - const {sortBy, sortOrder} = queryParams ?? {}; - - if (!sortBy && !sortOrder) { - return `search/${searchQuery}` as const; - } - - return `search/${searchQuery}?sortBy=${sortBy}&sortOrder=${sortOrder}` as const; - }, + route: 'search', + getRoute: ({query, isCustomQuery = false, policyIDs}: {query: SearchQueryString; isCustomQuery?: boolean; policyIDs?: string}) => + `search?q=${query}&isCustomQuery=${isCustomQuery}${policyIDs ? `&policyIDs=${policyIDs}` : ''}` as const, }, SEARCH_ADVANCED_FILTERS: 'search/filters', @@ -56,14 +48,11 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type', SEARCH_REPORT: { - route: 'search/:query/view/:reportID', - getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, + route: 'search/view/:reportID', + getRoute: (reportID: string) => `search/view/${reportID}` as const, }, - TRANSACTION_HOLD_REASON_RHP: { - route: 'search/:query/hold', - getRoute: (query: string) => `search/${query}/hold` as const, - }, + TRANSACTION_HOLD_REASON_RHP: 'search/hold', // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx index e3c795045048..abcd2df95e5c 100644 --- a/src/components/PromotedActionsBar.tsx +++ b/src/components/PromotedActionsBar.tsx @@ -7,7 +7,7 @@ import * as HeaderUtils from '@libs/HeaderUtils'; import * as Localize from '@libs/Localize'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types'; +import type {RootStackParamList, State} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActions from '@userActions/Report'; import * as Session from '@userActions/Session'; @@ -85,8 +85,7 @@ const PromotedActions = { return; } - const currentQuery = topmostCentralPaneRoute?.params as AuthScreensParamList['Search_Central_Pane']; - ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute(currentQuery?.query ?? CONST.SEARCH.TAB.ALL, reportAction?.childReportID ?? '')); + ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute(reportAction?.childReportID ?? '')); }, }), } satisfies PromotedActionsType; diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index bd4b843bbd60..172685377f97 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -12,12 +12,12 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchActions from '@libs/actions/Search'; import * as SearchUtils from '@libs/SearchUtils'; import CONST from '@src/CONST'; -import type {SearchDataTypes, SearchQuery, SearchReport} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes, SearchReport} from '@src/types/onyx/SearchResults'; import SearchPageHeader from './SearchPageHeader'; -import type {SelectedTransactionInfo, SelectedTransactions} from './types'; +import type {SearchStatus, SelectedTransactionInfo, SelectedTransactions} from './types'; type SearchListWithHeaderProps = Omit, 'onSelectAll' | 'onCheckboxPress' | 'sections'> & { - query: SearchQuery; + status: SearchStatus; hash: number; data: TransactionListItemType[] | ReportListItemType[]; searchType: SearchDataTypes; @@ -44,7 +44,7 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt } function SearchListWithHeader( - {ListItem, onSelectRow, query, hash, data, searchType, isMobileSelectionModeActive, setIsMobileSelectionModeActive, ...props}: SearchListWithHeaderProps, + {ListItem, onSelectRow, status, hash, data, searchType, isMobileSelectionModeActive, setIsMobileSelectionModeActive, ...props}: SearchListWithHeaderProps, ref: ForwardedRef, ) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -188,7 +188,7 @@ function SearchListWithHeader( ; clearSelectedItems?: () => void; @@ -39,7 +39,7 @@ type SearchPageHeaderProps = { type SearchHeaderOptionValue = DeepValueOf | undefined; function SearchPageHeader({ - query, + status, selectedTransactions = {}, hash, clearSelectedItems, @@ -58,7 +58,7 @@ function SearchPageHeader({ const {isSmallScreenWidth} = useResponsiveLayout(); const {setSelectedTransactionIDs} = useSearchContext(); - const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = { + const headerContent: {[key in SearchStatus]: {icon: IconAsset; title: string}} = { all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')}, shared: {icon: Illustrations.SendMoney, title: translate('common.shared')}, drafts: {icon: Illustrations.Pencil, title: translate('common.drafts')}, @@ -81,7 +81,7 @@ function SearchPageHeader({ return; } - SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? ''], () => { + SearchActions.exportSearchItemsToCSV(status, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? ''], () => { setDownloadErrorModalOpen?.(); }); }); @@ -109,7 +109,7 @@ function SearchPageHeader({ setIsMobileSelectionModeActive?.(false); } setSelectedTransactionIDs(selectedTransactionsKeys); - Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(query)); + Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP); }, }); } @@ -176,6 +176,7 @@ function SearchPageHeader({ return options; }, [ + status, selectedTransactionsKeys, selectedTransactions, translate, @@ -187,7 +188,6 @@ function SearchPageHeader({ theme.icon, styles.colorMuted, styles.fontWeightNormal, - query, isOffline, setOfflineModalOpen, setDownloadErrorModalOpen, @@ -211,8 +211,8 @@ function SearchPageHeader({ return ( {headerButtonsOptions.length > 0 && ( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ae38baf8f264..b8d7c02fc7d8 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -23,35 +23,35 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; -import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import {useSearchContext} from './SearchContext'; import SearchListWithHeader from './SearchListWithHeader'; import SearchPageHeader from './SearchPageHeader'; -import type {SearchColumnType, SortOrder} from './types'; +import type {SearchColumnType, SearchQueryJSON, SearchStatus, SortOrder} from './types'; type SearchProps = { - query: SearchQuery; + queryJSON: SearchQueryJSON; policyIDs?: string; - sortBy?: SearchColumnType; - sortOrder?: SortOrder; isMobileSelectionModeActive?: boolean; setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void; }; -const sortableSearchTabs: SearchQuery[] = [CONST.SEARCH.TAB.ALL]; +const sortableSearchTabs: SearchStatus[] = [CONST.SEARCH.STATUS.ALL]; const transactionItemMobileHeight = 100; const reportItemTransactionHeight = 52; const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item const searchHeaderHeight = 54; -function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchProps) { +function Search({queryJSON, policyIDs, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); const {isLargeScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); const {setCurrentSearchHash} = useSearchContext(); + const [offset, setOffset] = React.useState(0); + + const {status, sortBy, sortOrder, hash} = queryJSON; - const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder); const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); const getItemHeight = useCallback( @@ -96,9 +96,10 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv } setCurrentSearchHash(hash); - SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder}); + + SearchActions.search({hash, query: status, policyIDs, offset, sortBy, sortOrder}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [hash, isOffline]); + }, [hash, isOffline, offset]); const isDataLoaded = searchResults?.data !== undefined; const shouldShowLoadingState = !isOffline && !isDataLoaded; @@ -108,7 +109,7 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv return ( <> @@ -122,7 +123,7 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv return ( <> @@ -143,15 +144,14 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID); } - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(reportID)); }; const fetchMoreResults = () => { if (!searchResults?.search?.hasMoreResults || shouldShowLoadingState || shouldShowLoadingMoreItems) { return; } - const currentOffset = searchResults?.search?.offset ?? 0; - SearchActions.search({hash, query, offset: currentOffset + CONST.SEARCH.RESULTS_PAGE_SIZE, sortBy, sortOrder}); + setOffset(offset + CONST.SEARCH.RESULTS_PAGE_SIZE); }; const type = SearchUtils.getSearchType(searchResults?.search); @@ -167,13 +167,14 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv const sortedData = SearchUtils.getSortedSections(type, data, sortBy, sortOrder); const onSortPress = (column: SearchColumnType, order: SortOrder) => { - navigation.setParams({ - sortBy: column, - sortOrder: order, - }); + const currentSearchParams = SearchUtils.getCurrentSearchParams(); + const currentQueryJSON = SearchUtils.buildSearchQueryJSON(currentSearchParams.q, policyIDs); + + const newQuery = SearchUtils.buildSearchQueryString({...currentQueryJSON, sortBy: column, sortOrder: order}); + navigation.setParams({q: newQuery}); }; - const isSortingAllowed = sortableSearchTabs.includes(query); + const isSortingAllowed = sortableSearchTabs.includes(status); const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data); @@ -181,7 +182,7 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv return ( ; type SortOrder = ValueOf; type SearchColumnType = ValueOf; +type SearchStatus = ValueOf; type SearchContext = { currentSearchHash: number; @@ -49,4 +50,33 @@ type QueryFilters = { [K in AllFieldKeys]: QueryFilter | QueryFilter[]; }; -export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext, ASTNode, QueryFilter, QueryFilters, AllFieldKeys}; +type SearchQueryString = string; + +type SearchQueryAST = { + type: string; + status: SearchStatus; + sortBy: SearchColumnType; + sortOrder: SortOrder; + filters: ASTNode; +}; + +type SearchQueryJSON = { + input: string; + hash: number; +} & SearchQueryAST; + +export type { + SelectedTransactionInfo, + SelectedTransactions, + SearchColumnType, + SearchStatus, + SearchQueryAST, + SearchQueryJSON, + SearchQueryString, + SortOrder, + SearchContext, + ASTNode, + QueryFilter, + QueryFilters, + AllFieldKeys, +}; diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index be8ddfe43822..0be972b8b7ca 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getSearchParams} from '@libs/SearchUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import ActionCell from './ActionCell'; @@ -85,9 +84,7 @@ function ReportListItem({ }; const openReportInRHP = (transactionItem: TransactionListItemType) => { - const searchParams = getSearchParams(); - const currentQuery = searchParams?.query ?? CONST.SEARCH.TAB.ALL; - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(currentQuery, transactionItem.transactionThreadReportID)); + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(transactionItem.transactionThreadReportID)); }; if (!reportItem?.reportName && reportItem.transactions.length > 1) { diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts index 979a7b99886f..2659fac6810a 100644 --- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -1,7 +1,7 @@ -import type {SearchQuery} from '@src/types/onyx/SearchResults'; +import type {SearchStatus} from '@components/Search/types'; type ExportSearchItemsToCSVParams = { - query: SearchQuery; + query: SearchStatus; reportIDList: string[]; transactionIDList: string[]; policyIDs: string[]; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 476bdc999bc3..a43199c50439 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -22,6 +22,7 @@ import NetworkConnection from '@libs/NetworkConnection'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as ReportUtils from '@libs/ReportUtils'; +import {buildSearchQueryString} from '@libs/SearchUtils'; import * as SessionUtils from '@libs/SessionUtils'; import ConnectionCompletePage from '@pages/ConnectionCompletePage'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -87,7 +88,8 @@ function shouldOpenOnAdminRoom() { function getCentralPaneScreenInitialParams(screenName: CentralPaneName, initialReportID?: string): Partial> { if (screenName === SCREENS.SEARCH.CENTRAL_PANE) { - return {sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC}; + // Generate default query string with buildSearchQueryString without argument. + return {q: buildSearchQueryString()}; } if (screenName === SCREENS.REPORT) { diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index b09529fceaa2..8ec5b90e8d75 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -104,7 +104,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { if (selectedTab === SCREENS.SEARCH.BOTTOM_TAB) { return; } - interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL))); + interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}))); }} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.search')} diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4ce867884e32..2b5212330676 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1001,7 +1001,7 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { screens: { [SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route, - [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP.route, + [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, }, }, [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: { diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index 78b550f36303..28de413b0904 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -84,7 +84,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef>; const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fadce5727fce..145183430e4e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -11,7 +11,7 @@ import type { Route, } from '@react-navigation/native'; import type {TupleToUnion, ValueOf} from 'type-fest'; -import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import type {SearchQueryString} from '@components/Search/types'; import type {IOURequestType} from '@libs/actions/IOU'; import type CONST from '@src/CONST'; import type {Country, IOUAction, IOUType} from '@src/CONST'; @@ -64,12 +64,12 @@ type CentralPaneScreensParamList = { [SCREENS.SETTINGS.ABOUT]: undefined; [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; [SCREENS.SETTINGS.WORKSPACES]: undefined; + + // Param types of the search central pane are also used for the search bottom tab screen. [SCREENS.SEARCH.CENTRAL_PANE]: { - query: string; + q: SearchQueryString; + isCustomQuery: boolean; policyIDs?: string; - offset?: number; - sortBy?: SearchColumnType; - sortOrder?: SortOrder; }; [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; @@ -1194,13 +1194,7 @@ type ExplanationModalNavigatorParamList = { type BottomTabNavigatorParamList = { [SCREENS.HOME]: {policyID?: string}; - [SCREENS.SEARCH.BOTTOM_TAB]: { - query: string; - policyID?: string; - offset?: number; - sortBy?: SearchColumnType; - sortOrder?: SortOrder; - }; + [SCREENS.SEARCH.BOTTOM_TAB]: Omit & {policyID: string}; [SCREENS.SETTINGS.ROOT]: {policyID?: string}; }; @@ -1275,7 +1269,6 @@ type AuthScreensParamList = CentralPaneScreensParamList & type SearchReportParamList = { [SCREENS.SEARCH.REPORT_RHP]: { - query: string; reportID: string; }; }; diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 0957239b6acb..494ded6ae52d 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -21,7 +21,6 @@ "status": "all", "sortBy": "date", "sortOrder": "desc", - "offset": 0 }; function buildFilter(operator, left, right) { @@ -101,7 +100,6 @@ key / "keyword" { return "keyword"; } / "sortBy" { return "sortBy"; } / "sortOrder" { return "sortOrder"; } - / "offset" { return "offset"; } identifier = parts:(quotedString / alphanumeric)+ { return parts.join(''); } diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 4ec3fa9fb314..18888903053e 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,5 +1,5 @@ import type {ValueOf} from 'type-fest'; -import type {AllFieldKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SortOrder} from '@components/Search/types'; +import type {AllFieldKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types'; import ReportListItem from '@components/SelectionList/Search/ReportListItem'; import TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; @@ -299,7 +299,7 @@ function getSortedReportData(data: ReportListItemType[]) { }); } -function getSearchParams() { +function getCurrentSearchParams() { const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State); return topmostCentralPaneRoute?.params as AuthScreensParamList['Search_Central_Pane']; } @@ -308,37 +308,54 @@ function isSearchResultsEmpty(searchResults: SearchResults) { return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)); } -function getQueryHashFromString(query: string): number { +function getQueryHashFromString(query: SearchQueryString): number { return UserUtils.hashText(query, 2 ** 32); } -type JSONQuery = { - input: string; - hash: number; - type: string; - status: string; - sortBy: string; - sortOrder: string; - offset: number; - filters: ASTNode; -}; - -function buildJSONQuery(query: string) { +function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) { try { // Add the full input and hash to the results - const result = searchParser.parse(query) as JSONQuery; + const result = searchParser.parse(query) as SearchQueryJSON; result.input = query; - result.hash = getQueryHashFromString(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); return result; } catch (e) { console.error(e); } } -function getFilters(query: string, fields: Array>) { - let jsonQuery; +function buildSearchQueryString(partialQueryJSON?: Partial) { + 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 [, value] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) { + if (partialQueryJSON?.[value]) { + queryParts.push(`${value}:${partialQueryJSON[value]}`); + } else if (defaultQueryJSON) { + queryParts.push(`${value}:${defaultQueryJSON[value]}`); + } + } + + return queryParts.join(' '); +} + +/** + * Update string query with all the default params that are set by parser + */ +function normalizeQuery(query: string) { + const normalizedQueryJSON = buildSearchQueryJSON(query); + return buildSearchQueryString(normalizedQueryJSON); +} + +function getFilters(query: SearchQueryString, fields: Array>) { + let queryAST; + try { - jsonQuery = searchParser.parse(query) as JSONQuery; + queryAST = searchParser.parse(query) as SearchQueryJSON; } catch (e) { console.error(e); return; @@ -348,13 +365,13 @@ function getFilters(query: string, fields: Array>) { fields.forEach((field) => { const rootFieldKey = field as ValueOf; - if (jsonQuery[rootFieldKey] === undefined) { + if (queryAST[rootFieldKey] === undefined) { return; } filters[field] = { operator: 'eq', - value: jsonQuery[rootFieldKey], + value: queryAST[rootFieldKey], }; }); @@ -387,25 +404,27 @@ function getFilters(query: string, fields: Array>) { }); } - if (jsonQuery.filters) { - traverse(jsonQuery.filters); + if (queryAST.filters) { + traverse(queryAST.filters); } return filters; } export { - buildJSONQuery, + buildSearchQueryJSON, + buildSearchQueryString, + getCurrentSearchParams, getListItem, getQueryHash, getSections, getSortedSections, getShouldShowMerchant, getSearchType, - getSearchParams, shouldShowYear, isReportListItemType, isTransactionListItemType, isSearchResultsEmpty, getFilters, + normalizeQuery, }; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 245192ff6e0a..10af02f8b2a6 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,12 +1,14 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import type {FormOnyxValues} from '@components/Form/types'; +import type {SearchQueryString} from '@components/Search/types'; import * as API from '@libs/API'; import type {SearchParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; import fileDownload from '@libs/fileDownload'; import enhanceParameters from '@libs/Network/enhanceParameters'; +import {buildSearchQueryJSON} from '@libs/SearchUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; @@ -54,6 +56,22 @@ function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParam API.read(READ_COMMANDS.SEARCH, {hash, query, offset, policyIDs, sortBy, sortOrder}, {optimisticData, finallyData}); } +// TODO_SEARCH: use this function after backend changes. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function searchV2(queryString: SearchQueryString) { + const queryJSON = buildSearchQueryJSON(queryString); + + if (!queryJSON) { + return; + } + + const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash); + + // TODO_SEARCH: uncomment this line after backend changes + // @ts-expect-error waiting for backend changes + API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery: JSON.stringify(queryJSON)}, {optimisticData, finallyData}); +} + /** * It's possible that we return legacy transactions that don't have a transaction thread created yet. * In that case, when users select the search result row, we need to create the transaction thread on the fly and update the search result with the new transactionThreadReport @@ -90,9 +108,9 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[], onDownloadFailed: () => void) { +function exportSearchItemsToCSV(status: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[], onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { - query, + status, reportIDList, transactionIDList, policyIDs, diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 50044e784a63..d8d11662e34a 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -13,23 +13,20 @@ import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; type SearchHoldReasonPageRouteParams = { - /** ID of the transaction the page was opened for */ - transactionID: string; - /** Link to previous page */ backTo: Route; }; type SearchHoldReasonPageProps = { /** Navigation route context info provided by react navigation */ - route: RouteProp<{params: SearchHoldReasonPageRouteParams}>; + route: RouteProp<{params?: SearchHoldReasonPageRouteParams}>; }; function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); const {currentSearchHash, selectedTransactionIDs} = useSearchContext(); - const {backTo} = route.params; + const {backTo = ''} = route.params ?? {}; const onSubmit = (values: FormOnyxValues) => { SearchActions.holdMoneyRequestOnSearch(currentSearchHash, selectedTransactionIDs, values.comment); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 558830da983b..34a75300ef77 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useMemo} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; @@ -7,10 +7,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; +import {buildSearchQueryJSON} from '@libs/SearchUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {SearchQuery} from '@src/types/onyx/SearchResults'; type SearchPageProps = StackScreenProps; @@ -18,12 +18,11 @@ function SearchPage({route}: SearchPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); - const {query: rawQuery, policyIDs, sortBy, sortOrder} = route?.params ?? {}; + const {policyIDs} = route.params; - const query = rawQuery as SearchQuery; - const isValidQuery = Object.values(CONST.SEARCH.TAB).includes(query); + const queryJSON = useMemo(() => buildSearchQueryJSON(route.params.q, policyIDs), [route.params.q, policyIDs]); - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL)); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL})); // On small screens this page is not displayed, the configuration is in the file: src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx // To avoid calling hooks in the Search component when this page isn't visible, we return null here. @@ -39,16 +38,16 @@ function SearchPage({route}: SearchPageProps) { > - + {queryJSON && ( + + )} ); diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 3729d754fb61..5b135c751ffa 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,4 +1,3 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React, {useMemo, useState} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -9,22 +8,14 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; -import type {CentralPaneScreensParamList} from '@libs/Navigation/types'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import {buildSearchQueryJSON} from '@libs/SearchUtils'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {SearchQuery} from '@src/types/onyx/SearchResults'; -import SearchFilters from './SearchFilters'; +import SearchStatusMenu from './SearchStatusMenu'; -type SearchPageProps = StackScreenProps; - -const defaultSearchProps = { - query: '' as SearchQuery, - policyIDs: undefined, - sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, - sortOrder: CONST.SEARCH.SORT_ORDER.DESC, -}; function SearchPageBottomTab() { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -32,23 +23,21 @@ function SearchPageBottomTab() { const styles = useThemeStyles(); const [isMobileSelectionModeActive, setIsMobileSelectionModeActive] = useState(false); - const { - query: rawQuery, - policyIDs, - sortBy, - sortOrder, - } = useMemo(() => { - if (activeCentralPaneRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE || !activeCentralPaneRoute.params) { - return defaultSearchProps; + const {queryJSON, policyIDs} = useMemo(() => { + if (!activeCentralPaneRoute || activeCentralPaneRoute.name !== SCREENS.SEARCH.CENTRAL_PANE) { + return {queryJSON: undefined, policyIDs: undefined}; } - return {...defaultSearchProps, ...activeCentralPaneRoute.params} as SearchPageProps['route']['params']; - }, [activeCentralPaneRoute]); - const query = rawQuery as SearchQuery; + // This will be SEARCH_CENTRAL_PANE as we checked that in if. + const searchParams = activeCentralPaneRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; - const isValidQuery = Object.values(CONST.SEARCH.TAB).includes(query); + return { + queryJSON: buildSearchQueryJSON(searchParams.q, searchParams.policyIDs), + policyIDs: searchParams.policyIDs, + }; + }, [activeCentralPaneRoute]); - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL)); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL})); return ( - {!isMobileSelectionModeActive ? ( + {!isMobileSelectionModeActive && queryJSON ? ( <> - + ) : ( setIsMobileSelectionModeActive(false)} /> )} - {isSmallScreenWidth && ( + {isSmallScreenWidth && queryJSON && ( diff --git a/src/pages/Search/SearchFilters.tsx b/src/pages/Search/SearchStatusMenu.tsx similarity index 62% rename from src/pages/Search/SearchFilters.tsx rename to src/pages/Search/SearchStatusMenu.tsx index 26c22f7203b5..38ea2f7ef2a3 100644 --- a/src/pages/Search/SearchFilters.tsx +++ b/src/pages/Search/SearchStatusMenu.tsx @@ -1,69 +1,70 @@ import React from 'react'; import {View} from 'react-native'; import MenuItem from '@components/MenuItem'; +import type {SearchStatus} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import {normalizeQuery} from '@libs/SearchUtils'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type IconAsset from '@src/types/utils/IconAsset'; -import SearchFiltersNarrow from './SearchFiltersNarrow'; +import SearchStatusMenuNarrow from './SearchStatusMenuNarrow'; -type SearchFiltersProps = { - query: string; +type SearchStatusMenuProps = { + status: SearchStatus; }; -type SearchMenuFilterItem = { +type SearchStatusMenuItem = { title: string; - query: string; + status: SearchStatus; icon: IconAsset; route: Route; }; -// Because we will add have AdvancedFilters, in future rename this component to `SearchTypeMenu|Tabs|Filters` to avoid confusion -function SearchFilters({query}: SearchFiltersProps) { +function SearchStatusMenu({status}: SearchStatusMenuProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); - const filterItems: SearchMenuFilterItem[] = [ + const statusMenuItems: SearchStatusMenuItem[] = [ { title: translate('common.expenses'), - query: CONST.SEARCH.TAB.ALL, + status: CONST.SEARCH.STATUS.ALL, icon: Expensicons.Receipt, - route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.ALL)}), }, { title: translate('common.shared'), - query: CONST.SEARCH.TAB.SHARED, + status: CONST.SEARCH.STATUS.SHARED, icon: Expensicons.Send, - route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.SHARED), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.SHARED)}), }, { title: translate('common.drafts'), - query: CONST.SEARCH.TAB.DRAFTS, + status: CONST.SEARCH.STATUS.DRAFTS, icon: Expensicons.Pencil, - route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.DRAFTS), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.DRAFTS)}), }, { title: translate('common.finished'), - query: CONST.SEARCH.TAB.FINISHED, + status: CONST.SEARCH.STATUS.FINISHED, icon: Expensicons.CheckCircle, - route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.FINISHED), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.FINISHED)}), }, ]; - const activeItemIndex = filterItems.findIndex((item) => item.query === query); + const activeItemIndex = statusMenuItems.findIndex((item) => item.status === status); if (isSmallScreenWidth) { return ( - ); @@ -71,7 +72,7 @@ function SearchFilters({query}: SearchFiltersProps) { return ( - {filterItems.map((item, index) => { + {statusMenuItems.map((item, index) => { const onPress = singleExecution(() => Navigation.navigate(item.route)); return ( @@ -95,7 +96,7 @@ function SearchFilters({query}: SearchFiltersProps) { ); } -SearchFilters.displayName = 'SearchFilters'; +SearchStatusMenu.displayName = 'SearchStatusMenu'; -export default SearchFilters; -export type {SearchMenuFilterItem}; +export default SearchStatusMenu; +export type {SearchStatusMenuItem}; diff --git a/src/pages/Search/SearchFiltersNarrow.tsx b/src/pages/Search/SearchStatusMenuNarrow.tsx similarity index 88% rename from src/pages/Search/SearchFiltersNarrow.tsx rename to src/pages/Search/SearchStatusMenuNarrow.tsx index e890432df7f1..e80db44ce3dc 100644 --- a/src/pages/Search/SearchFiltersNarrow.tsx +++ b/src/pages/Search/SearchStatusMenuNarrow.tsx @@ -10,14 +10,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import * as Expensicons from '@src/components/Icon/Expensicons'; -import type {SearchMenuFilterItem} from './SearchFilters'; +import type {SearchStatusMenuItem} from './SearchStatusMenu'; -type SearchFiltersNarrowProps = { - filterItems: SearchMenuFilterItem[]; +type SearchStatusMenuNarrowProps = { + statusMenuItems: SearchStatusMenuItem[]; activeItemIndex: number; }; -function SearchFiltersNarrow({filterItems, activeItemIndex}: SearchFiltersNarrowProps) { +function SearchStatusMenuNarrow({statusMenuItems, activeItemIndex}: SearchStatusMenuNarrowProps) { const theme = useTheme(); const styles = useThemeStyles(); const {singleExecution} = useSingleExecution(); @@ -29,7 +29,7 @@ function SearchFiltersNarrow({filterItems, activeItemIndex}: SearchFiltersNarrow const openMenu = () => setIsPopoverVisible(true); const closeMenu = () => setIsPopoverVisible(false); - const popoverMenuItems = filterItems.map((item, index) => ({ + const popoverMenuItems = statusMenuItems.map((item, index) => ({ text: item.title, onSelected: singleExecution(() => Navigation.navigate(item.route)), icon: item.icon, @@ -77,6 +77,6 @@ function SearchFiltersNarrow({filterItems, activeItemIndex}: SearchFiltersNarrow ); } -SearchFiltersNarrow.displayName = 'SearchFiltersNarrow'; +SearchStatusMenuNarrow.displayName = 'SearchStatusMenuNarrow'; -export default SearchFiltersNarrow; +export default SearchStatusMenuNarrow; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index ccf838127a5e..049728a85614 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -148,7 +148,7 @@ function CardSection() { wrapperStyle={styles.sectionMenuItemTopDescription} title={translate('subscription.cardSection.viewPaymentHistory')} titleStyle={styles.textStrong} - onPress={() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL))} + onPress={() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}))} hoverAndPressStyle={styles.hoveredComponentBG} style={styles.mt5} /> diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 8496ce296eda..94fb182a4ad9 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -243,9 +243,6 @@ type SearchAccountDetails = Partial /** Types of searchable transactions */ type SearchTransactionType = ValueOf; -/** Types of search queries */ -type SearchQuery = ValueOf; - /** Model of search results */ type SearchResults = { /** Current search results state */ @@ -261,7 +258,6 @@ type SearchResults = { export default SearchResults; export type { - SearchQuery, SearchTransaction, SearchTransactionType, SearchTransactionAction,