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 v2] Add displaying advanced filter values and type/status #46022

Merged
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',
},
Comment on lines +5254 to +5256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB we should eventually move this to CONST.SEARCH.DATA_TYPES.EXPENSE and get rid of transactions and reports. We'll do so as part of v2.2.

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 @@ -47,6 +47,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 @@ -33,6 +33,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 @@ -1009,6 +1009,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}`;
}
Comment on lines +361 to +373
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: We can simplify buildQueryStringFromFilters function by extracting the date handling logic to a new buildDateFilterQuery function


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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 = () => {
Copy link
Contributor

@rayane-djouah rayane-djouah Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB:

Suggested change
const onFormSubmit = () => {
const openResults = () => {

From the Reviewer Checklist:

I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).

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
/>
Copy link
Contributor

@rayane-djouah rayane-djouah Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the "View results" button should not be disabled offline. based on OfflineUX_Patterns_Flowchart, No offline pattern is needed here.

Screenshot 2024-07-29 at 3 27 32 PM
Suggested change
/>
enabledWhenOffline
/>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep it as is for now so that we don't block other issues, but I think we should disable it offline because the Search API command only works while the user is online.

</View>
);
}
Expand Down
Loading
Loading