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

Create amount filter #47343

Merged
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,7 @@ const CONST = {
VISIBLE_PASSWORD: 'visible-password',
ASCII_CAPABLE: 'ascii-capable',
NUMBER_PAD: 'number-pad',
DECIMAL_PAD: 'decimal-pad',
},

INPUT_MODE: {
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant',
SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description',
SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID',
SEARCH_ADVANCED_FILTERS_AMOUNT: 'search/filters/amount',
SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category',
SEARCH_ADVANCED_FILTERS_KEYWORD: 'search/filters/keyword',
SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const SCREENS = {
ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP',
ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP',
ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP',
ADVANCED_FILTERS_AMOUNT_RHP: 'Search_Advanced_Filters_Amount_RHP',
ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP',
ADVANCED_FILTERS_KEYWORD_RHP: 'Search_Advanced_Filters_Keyword_RHP',
ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP',
Expand Down
66 changes: 66 additions & 0 deletions src/components/AmountWithoutCurrencyForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, {useCallback, useMemo} from 'react';
import type {ForwardedRef} from 'react';
import useLocalize from '@hooks/useLocalize';
import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
import CONST from '@src/CONST';
import TextInput from './TextInput';
import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types';

type AmountFormProps = {
/** Amount supplied by the FormProvider */
value?: string;

/** Callback to update the amount in the FormProvider */
onInputChange?: (value: string) => void;
} & Partial<BaseTextInputProps>;

function AmountWithoutCurrencyForm(
{value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps,
ref: ForwardedRef<BaseTextInputRef>,
) {
const {toLocaleDigit} = useLocalize();

const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);

/**
* Sets the selection and the amount accordingly to the value passed to the input
* @param newAmount - Changed amount from user input
*/
const setNewAmount = useCallback(
(newAmount: string) => {
// Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
// More info: https://github.com/Expensify/App/issues/16974
const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
const withLeadingZero = addLeadingZero(replacedCommasAmount);
if (!validateAmount(withLeadingZero, 2)) {
return;
}
onInputChange?.(withLeadingZero);
},
[onInputChange],
);

const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);

return (
<TextInput
value={formattedAmount}
onChangeText={setNewAmount}
inputID={inputID}
name={name}
label={label}
defaultValue={defaultValue}
accessibilityLabel={accessibilityLabel}
role={role}
ref={ref}
keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
);
}

AmountWithoutCurrencyForm.displayName = 'AmountWithoutCurrencyForm';

export default React.forwardRef(AmountWithoutCurrencyForm);
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3710,6 +3710,11 @@ export default {
keyword: 'Keyword',
hasKeywords: 'Has keywords',
currency: 'Currency',
amount: {
lessThan: (amount?: string) => `Less than ${amount ?? ''}`,
greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`,
between: (greaterThan: string, lessThan: string) => `Between ${greaterThan} and ${lessThan}`,
},
},
expenseType: 'Expense type',
},
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3764,6 +3764,11 @@ export default {
keyword: 'Palabra clave',
hasKeywords: 'Tiene palabras clave',
currency: 'Divisa',
amount: {
lessThan: (amount?: string) => `Menos de ${amount ?? ''}`,
greaterThan: (amount?: string) => `Más que ${amount ?? ''}`,
between: (greaterThan: string, lessThan: string) => `Entre ${greaterThan} y ${lessThan}`,
},
},
expenseType: 'Tipo de gasto',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersDescriptionPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersMerchantPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersReportIDPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersAmountPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersCategoryPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersKeywordPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersCardPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP,
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 @@ -1029,6 +1029,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_MERCHANT,
[SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DESCRIPTION,
[SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_REPORT_ID,
[SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_AMOUNT,
[SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CATEGORY,
[SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_KEYWORD,
[SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CARD,
Expand Down
81 changes: 52 additions & 29 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,28 @@ function buildDateFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>)
return dateFilter;
}

/**
* @private
* returns Date filter query string part, which needs special logic
*/
function buildAmountFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>) {
const lessThan = filterValues[FILTER_KEYS.LESS_THAN];
const greaterThan = filterValues[FILTER_KEYS.GREATER_THAN];

let amountFilter = '';
if (greaterThan) {
amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}>${greaterThan}`;

This comment was marked as resolved.

}
if (lessThan && greaterThan) {
amountFilter += ' ';
}
if (lessThan) {
amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}<${lessThan}`;

This comment was marked as resolved.

}

return amountFilter;
}

function sanitizeString(str: string) {
if (str.includes(' ') || str.includes(',')) {
return `"${str}"`;
Expand All @@ -464,42 +486,43 @@ function getExpenseTypeTranslationKey(expenseType: ValueOf<typeof CONST.SEARCH.T
* Given object with chosen search filters builds correct query string from them
*/
function buildQueryStringFromFilters(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 || filterKey === FILTER_KEYS.KEYWORD) && filterValue) {
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`;
}
const filtersString = Object.entries(filterValues).map(([filterKey, filterValue]) => {
if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.KEYWORD) && filterValue) {
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`;
}
}

if (
(filterKey === FILTER_KEYS.CATEGORY ||
filterKey === FILTER_KEYS.CARD_ID ||
filterKey === FILTER_KEYS.TAX_RATE ||
filterKey === FILTER_KEYS.EXPENSE_TYPE ||
filterKey === FILTER_KEYS.TAG ||
filterKey === FILTER_KEYS.CURRENCY ||
filterKey === FILTER_KEYS.FROM ||
filterKey === FILTER_KEYS.TO) &&
Array.isArray(filterValue) &&
filterValue.length > 0
) {
const filterValueArray = filterValues[filterKey] ?? [];
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
}
if (
(filterKey === FILTER_KEYS.CATEGORY ||
filterKey === FILTER_KEYS.CARD_ID ||
filterKey === FILTER_KEYS.TAX_RATE ||
filterKey === FILTER_KEYS.EXPENSE_TYPE ||
filterKey === FILTER_KEYS.TAG ||
filterKey === FILTER_KEYS.CURRENCY ||
filterKey === FILTER_KEYS.FROM ||
filterKey === FILTER_KEYS.TO) &&
Array.isArray(filterValue) &&
filterValue.length > 0
) {
const filterValueArray = filterValues[filterKey] ?? [];
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
}
}

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

const dateFilter = buildDateFilterQuery(filterValues);
filtersString.push(dateFilter);

const amountFilter = buildAmountFilterQuery(filterValues);
filtersString.push(amountFilter);

return dateFilter ? `${filtersString} ${dateFilter}` : filtersString;
return filtersString.filter(Boolean).join(' ');
}

function getFilters(queryJSON: SearchQueryJSON) {
Expand Down
19 changes: 19 additions & 0 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import {getAllTaxRates} from '@libs/PolicyUtils';
Expand Down Expand Up @@ -65,6 +66,19 @@ function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fiel
return dateValue;
}

if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
const {lessThan, greaterThan} = filters;
if (lessThan && greaterThan) {
return translate('search.filters.amount.between', convertToDisplayStringWithoutCurrency(Number(greaterThan)), convertToDisplayStringWithoutCurrency(Number(lessThan)));
}
if (lessThan) {
return translate('search.filters.amount.lessThan', convertToDisplayStringWithoutCurrency(Number(lessThan)));
}
if (greaterThan) {
return translate('search.filters.amount.greaterThan', convertToDisplayStringWithoutCurrency(Number(greaterThan)));
}
}

if (
(fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) &&
filters[fieldName]
Expand Down Expand Up @@ -149,6 +163,11 @@ function AdvancedSearchFilters() {
description: 'common.reportID' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_REPORT_ID,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT, translate),
description: 'common.total' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_AMOUNT,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY, translate),
description: 'common.category' as const,
Expand Down
91 changes: 91 additions & 0 deletions src/pages/Search/SearchFiltersAmountPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import AmountWithoutCurrencyForm from '@components/AmountWithoutCurrencyForm';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {updateAdvancedFilters} from '@libs/actions/Search';
import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm';

function SearchFiltersAmountPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();

const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const greaterThan = searchAdvancedFiltersForm?.[INPUT_IDS.GREATER_THAN];

This comment was marked as resolved.

const greaterThanFormattedAmount = greaterThan ? convertToFrontendAmountAsString(Number(greaterThan)) : undefined;
const lessThan = searchAdvancedFiltersForm?.[INPUT_IDS.LESS_THAN];

This comment was marked as resolved.

const lessThanFormattedAmount = lessThan ? convertToFrontendAmountAsString(Number(lessThan)) : undefined;
const {inputCallbackRef} = useAutoFocusInput();

const updateAmountFilter = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>) => {
const greater = values[INPUT_IDS.GREATER_THAN];
const greaterThanBackendAmount = greater ? convertToBackendAmount(Number(greater)) : '';
const less = values[INPUT_IDS.LESS_THAN];
const lessThanBackendAmount = less ? convertToBackendAmount(Number(less)) : '';
updateAdvancedFilters({greaterThan: greaterThanBackendAmount?.toString(), lessThan: lessThanBackendAmount?.toString()});
Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
};

return (
<ScreenWrapper
testID={SearchFiltersAmountPage.displayName}
shouldShowOfflineIndicatorInWideScreen
offlineIndicatorStyle={styles.mtAuto}
includeSafeAreaPaddingBottom={false}
>
<HeaderWithBackButton
title={translate('common.total')}
onBackButtonPress={() => {
Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
}}
/>
<FormProvider
style={[styles.flex1, styles.ph5]}
formID={ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM}
onSubmit={updateAmountFilter}
submitButtonText={translate('common.save')}
enabledWhenOffline
>
<View style={styles.mb5}>
<InputWrapper
InputComponent={AmountWithoutCurrencyForm}
inputID={INPUT_IDS.GREATER_THAN}
name={INPUT_IDS.GREATER_THAN}
defaultValue={greaterThanFormattedAmount}
label={translate('search.filters.amount.greaterThan')}
accessibilityLabel={translate('search.filters.amount.greaterThan')}
role={CONST.ROLE.PRESENTATION}
ref={inputCallbackRef}
/>
</View>
<View style={styles.mb5}>
<InputWrapper
InputComponent={AmountWithoutCurrencyForm}
inputID={INPUT_IDS.LESS_THAN}
name={INPUT_IDS.LESS_THAN}
defaultValue={lessThanFormattedAmount}
label={translate('search.filters.amount.lessThan')}
accessibilityLabel={translate('search.filters.amount.lessThan')}
role={CONST.ROLE.PRESENTATION}
/>
</View>
</FormProvider>
</ScreenWrapper>
);
}

SearchFiltersAmountPage.displayName = 'SearchFiltersAmountPage';

export default SearchFiltersAmountPage;
4 changes: 4 additions & 0 deletions src/types/form/SearchAdvancedFiltersForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const FILTER_KEYS = {
MERCHANT: 'merchant',
DESCRIPTION: 'description',
REPORT_ID: 'reportID',
LESS_THAN: 'lessThan',
GREATER_THAN: 'greaterThan',
TAX_RATE: 'taxRate',
EXPENSE_TYPE: 'expenseType',
TAG: 'tag',
Expand All @@ -33,6 +35,8 @@ type SearchAdvancedFiltersForm = Form<
[FILTER_KEYS.MERCHANT]: string;
[FILTER_KEYS.DESCRIPTION]: string;
[FILTER_KEYS.REPORT_ID]: string;
[FILTER_KEYS.LESS_THAN]: string;
[FILTER_KEYS.GREATER_THAN]: string;
[FILTER_KEYS.KEYWORD]: string;
[FILTER_KEYS.TAX_RATE]: string[];
[FILTER_KEYS.EXPENSE_TYPE]: string[];
Expand Down
Loading