diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index c2d7b52fa3e9..af952613f48f 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -30,15 +30,14 @@ import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; import {useSearchContext} from './SearchContext'; import SearchPageHeader from './SearchPageHeader'; -import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; +import type {SearchColumnType, SearchQueryJSON, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; type SearchProps = { queryJSON: SearchQueryJSON; - policyIDs?: string; isCustomQuery: boolean; + policyIDs?: string; }; -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 @@ -49,7 +48,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri } function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) { - return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected}; + return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected}; } function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) { @@ -58,7 +57,7 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt : { ...item, transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)), - isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected), + isSelected: item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected), }; } @@ -87,19 +86,14 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); - const {status, sortBy, sortOrder, hash} = queryJSON; + const {sortBy, sortOrder, hash} = queryJSON; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); useEffect(() => { - if (isSmallScreenWidth) { - return; - } clearSelectedTransactions(hash); setCurrentSearchHash(hash); - - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [hash]); + }, [hash, clearSelectedTransactions, setCurrentSearchHash]); useEffect(() => { const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); @@ -114,6 +108,15 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { } }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); + useEffect(() => { + if (isOffline) { + return; + } + + SearchActions.search({queryJSON, offset, policyIDs}); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isOffline, offset, queryJSON]); + const handleOnCancelConfirmModal = () => { setSelectedTransactionsToDelete([]); setDeleteExpensesConfirmModalVisible(false); @@ -134,19 +137,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { setDeleteExpensesConfirmModalVisible(true); }; - useEffect(() => { - const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); - if (!isSmallScreenWidth) { - if (selectedKeys.length === 0) { - turnOffMobileSelectionMode(); - } - return; - } - if (selectedKeys.length > 0 && !selectionMode?.isEnabled) { - turnOnMobileSelectionMode(); - } - }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); - const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType) => { if (SearchUtils.isTransactionListItemType(item)) { @@ -183,15 +173,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current; - useEffect(() => { - if (isOffline) { - return; - } - - SearchActions.search({hash, query: status, policyIDs, offset, sortBy, sortOrder}); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [hash, isOffline, offset]); - const isDataLoaded = searchResults?.data !== undefined; const shouldShowLoadingState = !isOffline && !isDataLoaded; const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; @@ -306,15 +287,10 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { }; const onSortPress = (column: SearchColumnType, order: SortOrder) => { - const currentSearchParams = SearchUtils.getCurrentSearchParams(); - const currentQueryJSON = SearchUtils.buildSearchQueryJSON(currentSearchParams.q, policyIDs); - - const newQuery = SearchUtils.buildSearchQueryString({...currentQueryJSON, sortBy: column, sortOrder: order}); + const newQuery = SearchUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); navigation.setParams({q: newQuery}); }; - const isSortingAllowed = sortableSearchTabs.includes(status); - const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data); const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true; @@ -344,7 +320,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) { metadata={searchResults?.search} onSortPress={onSortPress} sortOrder={sortOrder} - isSortingAllowed={isSortingAllowed} sortBy={sortBy} shouldShowYear={shouldShowYear} /> diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 52adf28166f7..96b9b3063b48 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -48,7 +48,7 @@ type QueryFilter = { type AdvancedFiltersKeys = ValueOf | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS; type QueryFilters = { - [K in AdvancedFiltersKeys]?: QueryFilter | QueryFilter[]; + [K in AdvancedFiltersKeys]?: QueryFilter[]; }; type SearchQueryString = string; diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 4bf1715e0434..a260ee6bd0f0 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -90,12 +90,11 @@ type SearchTableHeaderProps = { metadata: OnyxTypes.SearchResults['search']; sortBy?: SearchColumnType; sortOrder?: SortOrder; - isSortingAllowed: boolean; onSortPress: (column: SearchColumnType, order: SortOrder) => void; shouldShowYear: boolean; }; -function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed, onSortPress, shouldShowYear}: SearchTableHeaderProps) { +function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shouldShowYear}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); @@ -116,7 +115,6 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed, const isActive = sortBy === columnName; const textStyle = columnName === CONST.SEARCH.TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null; - const isSortable = isSortingAllowed && isColumnSortable; return ( onSortPress(columnName, order)} /> ); diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts index 60ea54419492..530388dc7f47 100644 --- a/src/libs/API/parameters/Search.ts +++ b/src/libs/API/parameters/Search.ts @@ -1,12 +1,10 @@ -import type {SortOrder} from '@components/Search/types'; +import type {SearchQueryString} from '@components/Search/types'; type SearchParams = { hash: number; - query: string; + jsonQuery: SearchQueryString; + // Tod this is temporary, remove top level policyIDs as part of: https://github.com/Expensify/App/issues/46592 policyIDs?: string; - sortBy?: string; - sortOrder?: SortOrder; - offset: number; }; export default SearchParams; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 6710e2df5a47..053806eab56d 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,5 +1,5 @@ import type {ValueOf} from 'type-fest'; -import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types'; +import type {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'; @@ -341,16 +341,31 @@ function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) { } } -function buildSearchQueryString(partialQueryJSON?: Partial) { +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 [, value] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) { - if (partialQueryJSON?.[value]) { - queryParts.push(`${value}:${partialQueryJSON[value]}`); + for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) { + if (queryJSON?.[key]) { + queryParts.push(`${key}:${queryJSON[key]}`); } else if (defaultQueryJSON) { - queryParts.push(`${value}:${defaultQueryJSON[value]}`); + queryParts.push(`${key}:${defaultQueryJSON[key]}`); + } + } + + if (!queryJSON) { + return queryParts.join(' '); + } + + const filters = getFilters(queryJSON); + + for (const [, filterKey] of Object.entries(CONST.SEARCH.SYNTAX_FILTER_KEYS)) { + const queryFilter = filters[filterKey]; + + if (queryFilter) { + const filterValueString = buildFilterString(filterKey, queryFilter); + queryParts.push(filterValueString); } } @@ -424,29 +439,9 @@ function buildQueryStringFromFilters(filterValues: Partial>) { - let queryAST; - - try { - queryAST = searchParser.parse(query) as SearchQueryJSON; - } catch (e) { - console.error(e); - return; - } - +function getFilters(queryJSON: SearchQueryJSON) { const filters = {} as QueryFilters; - - fields.forEach((field) => { - const rootFieldKey = field as ValueOf; - if (queryAST[rootFieldKey] === undefined) { - return; - } - - filters[field] = { - operator: 'eq', - value: queryAST[rootFieldKey], - }; - }); + const filterKeys = Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS); function traverse(node: ASTNode) { if (!node.operator) { @@ -462,7 +457,7 @@ function getFilters(query: SearchQueryString, fields: Array; - if (!fields.includes(nodeKey)) { + if (!filterKeys.includes(nodeKey)) { return; } @@ -470,26 +465,27 @@ function getFilters(query: SearchQueryString, fields: Array { // If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value if ((queryFilter.operator === 'eq' && queryFilters[index - 1]?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters[index - 1]?.operator === 'neq')) { - filterValueString += `,${queryFilter.value}`; + filterValueString += `,${filterName}:${queryFilter.value}`; } else { filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${queryFilter.value}`; } @@ -499,14 +495,14 @@ function buildFilterValueString(filterName: string, queryFilters: QueryFilter[]) } function getSearchHeaderTitle(queryJSON: SearchQueryJSON) { - const {inputQuery, type, status} = queryJSON; - const filters = getFilters(inputQuery, Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS)) ?? {}; + const {type, status} = queryJSON; + const filters = getFilters(queryJSON) ?? {}; let title = `type:${type} status:${status}`; Object.keys(filters).forEach((key) => { - const queryFilter = filters[key as ValueOf] as QueryFilter[]; - title += buildFilterValueString(key, queryFilter); + const queryFilter = filters[key as ValueOf] ?? []; + title += buildFilterString(key, queryFilter); }); return title; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 4b782e8b103c..040fd6e47491 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,14 +1,13 @@ 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 type {SearchQueryJSON} from '@components/Search/types'; import * as API from '@libs/API'; -import type {ExportSearchItemsToCSVParams, SearchParams} from '@libs/API/parameters'; +import type {ExportSearchItemsToCSVParams} 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'; @@ -50,26 +49,16 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall return {optimisticData, finallyData}; } -function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParams) { - const {optimisticData, finallyData} = getOnyxLoadingData(hash); - - 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; - } - +function search({queryJSON, offset, policyIDs}: {queryJSON: SearchQueryJSON; offset?: number; policyIDs?: string}) { 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}); + const queryWithOffset = { + ...queryJSON, + offset, + }; + const jsonQuery = JSON.stringify(queryWithOffset); + + API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery, policyIDs}, {optimisticData, finallyData}); } /** diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index f80aed9b8c28..1b9e81c8a035 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -80,8 +80,8 @@ function SearchPageBottomTab() { {shouldUseNarrowLayout && queryJSON && ( )}