Skip to content

Commit

Permalink
Merge pull request Expensify#46022 from software-mansion-labs/kicu/45…
Browse files Browse the repository at this point in the history
…026-advanced-filter-values

[Search v2] Add displaying advanced filter values and type/status
  • Loading branch information
luacmartins authored Jul 29, 2024
2 parents 7e1af70 + 153102f commit b4c46f2
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5251,6 +5251,9 @@ const CONST = {
DRAFTS: 'drafts',
FINISHED: 'finished',
},
TYPE: {
EXPENSE: 'expense',
},
TAB: {
EXPENSE: {
ALL: 'type:expense status:all',
Expand Down
2 changes: 2 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const ROUTES = {

SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type',

SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status',

SEARCH_REPORT: {
route: 'search/view/:reportID',
getRoute: (reportID: string) => `search/view/${reportID}` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const SCREENS = {
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
Expand Down
1 change: 1 addition & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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({queryJSON, policyIDs, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchProps) {
const {isOffline} = useNetwork();
const styles = useThemeStyles();
Expand Down
8 changes: 4 additions & 4 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ type QueryFilter = {
value: string | number;
};

type AllFieldKeys = ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS> | ValueOf<typeof CONST.SEARCH.SYNTAX_ROOT_KEYS>;
type AdvancedFiltersKeys = ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS> | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS;

type QueryFilters = {
[K in AllFieldKeys]: QueryFilter | QueryFilter[];
[K in AdvancedFiltersKeys]?: QueryFilter | QueryFilter[];
};

type SearchQueryString = string;
Expand All @@ -61,7 +61,7 @@ type SearchQueryAST = {
};

type SearchQueryJSON = {
input: string;
inputQuery: SearchQueryString;
hash: number;
} & SearchQueryAST;

Expand All @@ -78,5 +78,5 @@ export type {
ASTNode,
QueryFilter,
QueryFilters,
AllFieldKeys,
AdvancedFiltersKeys,
};
6 changes: 4 additions & 2 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3570,6 +3570,7 @@ export default {
search: {
selectMultiple: 'Select multiple',
resultsAreLimited: 'Search results are limited.',
viewResults: 'View results',
searchResults: {
emptyResults: {
title: 'Nothing to show',
Expand All @@ -3587,9 +3588,10 @@ export default {
filtersHeader: 'Filters',
filters: {
date: {
before: 'Before',
after: 'After',
before: (date?: string) => `Before ${date ?? ''}`,
after: (date?: string) => `After ${date ?? ''}`,
},
status: 'Status',
},
},
genericErrorPage: {
Expand Down
6 changes: 4 additions & 2 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,7 @@ export default {
search: {
selectMultiple: 'Seleccionar varios',
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
viewResults: 'Ver resultados',
searchResults: {
emptyResults: {
title: 'No hay nada que ver aquí',
Expand All @@ -3644,9 +3645,10 @@ export default {
filtersHeader: 'Filtros',
filters: {
date: {
before: 'Antes de',
after: 'Después de',
before: (date?: string) => `Antes de ${date ?? ''}`,
after: (date?: string) => `Después de ${date ?? ''}`,
},
status: 'Estado',
},
},
genericErrorPage: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchAdvancedFiltersPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersDatePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersTypePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersStatusPage').default,
});

const RestrictedActionModalStackNavigator = createModalStackNavigator<SearchReportParamList>({
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
},
},
[SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: {
Expand Down
56 changes: 53 additions & 3 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type {ValueOf} from 'type-fest';
import type {AllFieldKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types';
import type {AdvancedFiltersKeys, 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';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm';
import type * as OnyxTypes from '@src/types/onyx';
import type {SearchAccountDetails, SearchDataTypes, SearchPersonalDetails, SearchTransaction, SearchTypeToItemMap, SectionsType} from '@src/types/onyx/SearchResults';
import type SearchResults from '@src/types/onyx/SearchResults';
Expand Down Expand Up @@ -316,7 +318,7 @@ function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) {
try {
// Add the full input and hash to the results
const result = searchParser.parse(query) as SearchQueryJSON;
result.input = query;
result.inputQuery = query;

// Temporary solution until we move policyID filter into the AST - then remove this line and keep only query
const policyIDPart = policyID ?? '';
Expand Down Expand Up @@ -351,7 +353,54 @@ function normalizeQuery(query: string) {
return buildSearchQueryString(normalizedQueryJSON);
}

function getFilters(query: SearchQueryString, fields: Array<Partial<AllFieldKeys>>) {
/**
* @private
* returns Date filter query string part, which needs special logic
*/
function buildDateFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>) {
const dateBefore = filterValues[INPUT_IDS.DATE_BEFORE];
const dateAfter = filterValues[INPUT_IDS.DATE_AFTER];

let dateFilter = '';
if (dateBefore) {
dateFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE}<${dateBefore}`;
}
if (dateBefore && dateAfter) {
dateFilter += ' ';
}
if (dateAfter) {
dateFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE}>${dateAfter}`;
}

return dateFilter;
}

/**
* Given object with chosen search filters builds correct query string from them
*/
function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFiltersForm>) {
// TODO add handling of multiple values picked
const filtersString = Object.entries(filterValues)
.map(([filterKey, filterValue]) => {
if (filterKey === INPUT_IDS.TYPE && filterValue) {
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${filterValue as string}`;
}

if (filterKey === INPUT_IDS.STATUS && filterValue) {
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${filterValue as string}`;
}

return undefined;
})
.filter(Boolean)
.join(' ');

const dateFilter = buildDateFilterQuery(filterValues);

return dateFilter ? `${filtersString} ${dateFilter}` : filtersString;
}

function getFilters(query: SearchQueryString, fields: Array<Partial<AdvancedFiltersKeys>>) {
let queryAST;

try {
Expand Down Expand Up @@ -427,4 +476,5 @@ export {
isSearchResultsEmpty,
getFilters,
normalizeQuery,
buildQueryStringFromFilters,
};
22 changes: 19 additions & 3 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,26 @@ function exportSearchItemsToCSV({query, reportIDList, transactionIDList, policyI
}

/**
* Updates the form values for the advanced search form.
* Updates the form values for the advanced filters search form.
*/
function updateAdvancedFilters(values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>) {
function updateAdvancedFilters(values: Partial<FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>>) {
Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values);
}

export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV, updateAdvancedFilters};
/**
* Clears all values for the advanced filters search form.
*/
function clearAdvancedFilters() {
Onyx.set(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, null);
}

export {
search,
createTransactionThread,
deleteMoneyRequestOnSearch,
holdMoneyRequestOnSearch,
unholdMoneyRequestOnSearch,
exportSearchItemsToCSV,
updateAdvancedFilters,
clearAdvancedFilters,
};
99 changes: 78 additions & 21 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,111 @@
import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import type {AdvancedFiltersKeys} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import Navigation from '@libs/Navigation/Navigation';
import * as SearchUtils from '@libs/SearchUtils';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';

function getFilterDisplayTitle(filters: Record<string, string>, fieldName: string) {
// This is temporary because the full parsing of search query is not yet done
// TODO once we have values from query, this value should be `filters[fieldName].value`
return fieldName;
function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fieldName: AdvancedFiltersKeys, translate: LocaleContextProps['translate']) {
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) {
// the value of date filter is a combination of dateBefore + dateAfter values
const {dateAfter, dateBefore} = filters;
let dateValue = '';
if (dateBefore) {
dateValue = translate('search.filters.date.before', dateBefore);
}
if (dateBefore && dateAfter) {
dateValue += ', ';
}
if (dateAfter) {
dateValue += translate('search.filters.date.after', dateAfter);
}

return dateValue;
}

// Todo Once all Advanced filters are implemented this line can be cleaned up. See: https://github.com/Expensify/App/issues/45026
// @ts-expect-error this property access is temporarily an error, because not every SYNTAX_FILTER_KEYS is handled by form.
// When all filters are updated here: src/types/form/SearchAdvancedFiltersForm.ts this line comment + type cast can be removed.
const filterValue = filters[fieldName] as string;
return filterValue ? Str.recapitalize(filterValue) : undefined;
}

function AdvancedSearchFilters() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();

const [searchAdvancedFilters = {}] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);

const advancedFilters = useMemo(
() => [
{
title: getFilterDisplayTitle({}, 'title'),
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE, translate),
description: 'common.type' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
},
{
title: getFilterDisplayTitle({}, 'date'),
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS, translate),
description: 'search.filters.status' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, translate),
description: 'common.date' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
},
],
[],
[searchAdvancedFilters, translate],
);

const onFormSubmit = () => {
const query = SearchUtils.buildQueryStringFromFilters(searchAdvancedFilters);
SearchActions.clearAdvancedFilters();
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
query,
isCustomQuery: true,
}),
);
};

return (
<View>
{advancedFilters.map((item) => {
const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));

return (
<MenuItemWithTopDescription
key={item.description}
title={item.title}
description={translate(item.description)}
shouldShowRightIcon
onPress={onPress}
/>
);
})}
<View style={[styles.flex1, styles.justifyContentBetween]}>
<View>
{advancedFilters.map((item) => {
const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));

return (
<MenuItemWithTopDescription
key={item.description}
title={item.title}
description={translate(item.description)}
shouldShowRightIcon
onPress={onPress}
/>
);
})}
</View>
<FormAlertWithSubmitButton
buttonText={translate('search.viewResults')}
containerStyles={[styles.m4]}
onSubmit={onFormSubmit}
enabledWhenOffline
/>
</View>
);
}
Expand Down
Loading

0 comments on commit b4c46f2

Please sign in to comment.