Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search Results Page Header Cleanup #48538

8 changes: 7 additions & 1 deletion src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/typ
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import {usePersonalDetails} from '@components/OnyxProvider';
import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
Expand All @@ -21,6 +22,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as SearchActions from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import * as SearchUtils from '@libs/SearchUtils';
import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import variables from '@styles/variables';
Expand Down Expand Up @@ -126,6 +128,10 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {selectedTransactions, clearSelectedTransactions} = useSearchContext();
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const taxRates = getAllTaxRates();
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);

const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {});

Expand All @@ -145,7 +151,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa

const isCannedQuery = SearchUtils.isCannedSearchQuery(queryJSON);

const headerSubtitle = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchUtils.getSearchHeaderTitle(queryJSON);
const headerSubtitle = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);
const headerTitle = isCannedQuery ? '' : translate('search.filtersHeader');
const headerIcon = isCannedQuery ? getHeaderContent(type).icon : Illustrations.Filters;

Expand Down
1 change: 1 addition & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type SearchQueryAST = {
type SearchQueryJSON = {
inputQuery: SearchQueryString;
hash: number;
flatFilters: QueryFilters;
} & SearchQueryAST;

export type {
Expand Down
154 changes: 98 additions & 56 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SearchStatus, SortOrder} from '@components/Search/types';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
Expand All @@ -17,6 +18,8 @@ import DateUtils from './DateUtils';
import {translateLocal} from './Localize';
import navigationRef from './Navigation/navigationRef';
import type {AuthScreensParamList, RootStackParamList, State} from './Navigation/types';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as ReportUtils from './ReportUtils';
import * as searchParser from './SearchParser/searchParser';
import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
Expand Down Expand Up @@ -305,50 +308,6 @@ function getQueryHashFromString(query: SearchQueryString): number {
return UserUtils.hashText(query, 2 ** 32);
}

function buildSearchQueryJSON(query: SearchQueryString) {
try {
const result = searchParser.parse(query) as SearchQueryJSON;

// Add the full input and hash to the results
result.inputQuery = query;
result.hash = getQueryHashFromString(query);
return result;
} catch (e) {
console.error(`Error when parsing SearchQuery: "${query}"`, e);
}
}

function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryParts: string[] = [];
const defaultQueryJSON = buildSearchQueryJSON('');

for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) {
const existingFieldValue = queryJSON?.[key];
const queryFieldValue = existingFieldValue ?? defaultQueryJSON?.[key];

if (queryFieldValue) {
queryParts.push(`${key}:${queryFieldValue}`);
}
}

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);
}
}

return queryParts.join(' ');
}

/**
* Update string query with all the default params that are set by parser
*/
Expand Down Expand Up @@ -442,10 +401,56 @@ function getChatStatusTranslationKey(chatStatus: ValueOf<typeof CONST.SEARCH.CHA
}
}

function buildSearchQueryJSON(query: SearchQueryString) {
try {
const result = searchParser.parse(query) as SearchQueryJSON;
const flatFilters = getFilters(result);

// Add the full input and hash to the results
result.inputQuery = query;
result.hash = getQueryHashFromString(query);
result.flatFilters = flatFilters;
return result;
} catch (e) {
console.error(`Error when parsing SearchQuery: "${query}"`, e);
}
}

function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryParts: string[] = [];
const defaultQueryJSON = buildSearchQueryJSON('');

for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) {
const existingFieldValue = queryJSON?.[key];
const queryFieldValue = existingFieldValue ?? defaultQueryJSON?.[key];

if (queryFieldValue) {
queryParts.push(`${key}:${queryFieldValue}`);
}
}

if (!queryJSON) {
return queryParts.join(' ');
}

const filters = queryJSON.flatFilters;

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);
}
}

return queryParts.join(' ');
}

/**
* Given object with chosen search filters builds correct query string from them
*/
function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFiltersForm>) {
function buildQueryStringFromFilterValues(filterValues: Partial<SearchAdvancedFiltersForm>) {
const filtersString = Object.entries(filterValues).map(([filterKey, filterValue]) => {
if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID) && filterValue) {
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
Expand Down Expand Up @@ -494,6 +499,11 @@ function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFilters
return filtersString.filter(Boolean).join(' ');
}

/**
*
* @private
* traverses the AST and returns filters as a QueryFilters object
*/
function getFilters(queryJSON: SearchQueryJSON) {
const filters = {} as QueryFilters;
const filterKeys = Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS);
Expand Down Expand Up @@ -555,29 +565,62 @@ function getPolicyIDFromSearchQuery(queryJSON: SearchQueryJSON) {
return policyID;
}

function buildFilterString(filterName: string, queryFilters: QueryFilter[]) {
function getDisplayValue(filterName: string, filter: string, personalDetails: OnyxTypes.PersonalDetailsList, cardList: OnyxTypes.CardList, reports: OnyxCollection<OnyxTypes.Report>) {
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
return PersonalDetailsUtils.createDisplayName(personalDetails[filter]?.login ?? '', personalDetails[filter]);
}
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) {
return cardList[filter].bank;
}
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) {
return ReportUtils.getReportName(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${filter}`]);
}
return filter;
}

function buildFilterString(filterName: string, queryFilters: QueryFilter[], delimiter = ',') {
let filterValueString = '';
queryFilters.forEach((queryFilter, index) => {
// 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 += ` ${sanitizeString(queryFilter.value.toString())}`;
filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`;
} else {
filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${queryFilter.value}`;
filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${sanitizeString(queryFilter.value.toString())}`;
}
});

return filterValueString;
}

function getSearchHeaderTitle(queryJSON: SearchQueryJSON) {
const {type, status} = queryJSON;
const filters = getFilters(queryJSON) ?? {};

let title = `type:${type} status:${status}`;
function getSearchHeaderTitle(
queryJSON: SearchQueryJSON,
PersonalDetails: OnyxTypes.PersonalDetailsList,
cardList: OnyxTypes.CardList,
reports: OnyxCollection<OnyxTypes.Report>,
TaxRates: Record<string, string[]>,
) {
const filters = queryJSON.flatFilters ?? {};
let title = '';

Object.keys(filters).forEach((key) => {
const queryFilter = filters[key as ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>] ?? [];
title += buildFilterString(key, queryFilter);
let displayQueryFilters: QueryFilter[] = [];
if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
const taxRateIDs = queryFilter.map((filter) => filter.value.toString());
const taxRateNames = Object.entries(TaxRates)
.filter(([, taxRateKeys]) => taxRateKeys.some((taxID) => taxRateIDs.includes(taxID)))
.map(([taxRate]) => taxRate);
displayQueryFilters = taxRateNames.map((taxRate) => ({
operator: queryFilter[0].operator,
value: taxRate,
}));
} else {
displayQueryFilters = queryFilter.map((filter) => ({
operator: filter.operator,
value: getDisplayValue(key, filter.value.toString(), PersonalDetails, cardList, reports),
}));
}
title += buildFilterString(key, displayQueryFilters, ' ');
});

return title;
Expand All @@ -598,11 +641,10 @@ function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
}

export {
buildQueryStringFromFilters,
buildQueryStringFromFilterValues,
buildSearchQueryJSON,
buildSearchQueryString,
getCurrentSearchParams,
getFilters,
getPolicyIDFromSearchQuery,
getListItem,
getSearchHeaderTitle,
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall

function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) {
const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash);

const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON;
const queryWithOffset = {
...queryJSON,
...queryJSONWithoutFlatFilters,
offset,
};
const jsonQuery = JSON.stringify(queryWithOffset);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ function AdvancedSearchFilters() {
const currentType = searchAdvancedFilters?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE;

const onFormSubmit = () => {
const query = SearchUtils.buildQueryStringFromFilters(searchAdvancedFilters);
const query = SearchUtils.buildQueryStringFromFilterValues(searchAdvancedFilters);
SearchActions.clearAdvancedFilters();
Navigation.dismissModal();
Navigation.navigate(
Expand Down
10 changes: 9 additions & 1 deletion src/pages/Search/SearchTypeMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import MenuItem from '@components/MenuItem';
import {usePersonalDetails} from '@components/OnyxProvider';
import type {SearchQueryJSON} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import * as SearchUtils from '@libs/SearchUtils';
import variables from '@styles/variables';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
Expand All @@ -34,6 +38,10 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {singleExecution} = useSingleExecution();
const {translate} = useLocalize();
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const taxRates = getAllTaxRates();
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);

const typeMenuItems: SearchTypeMenuItem[] = [
{
Expand All @@ -60,7 +68,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => item.type === type) : -1;

if (shouldUseNarrowLayout) {
const title = isCannedQuery ? undefined : SearchUtils.getSearchHeaderTitle(queryJSON);
const title = isCannedQuery ? undefined : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);

return (
<SearchTypeMenuNarrow
Expand Down
Loading