Skip to content

Commit

Permalink
Merge pull request #47343 from software-mansion-labs/@szymczak/amount…
Browse files Browse the repository at this point in the history
…-filter

Create amount filter
  • Loading branch information
luacmartins authored Aug 21, 2024
2 parents d7f7fc2 + c205d92 commit 42e8486
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 29 deletions.
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 @@ -3719,6 +3719,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 @@ -3770,6 +3770,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 @@ -50,6 +50,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}`;
}
if (lessThan && greaterThan) {
amountFilter += ' ';
}
if (lessThan) {
amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}<${lessThan}`;
}

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];
const greaterThanFormattedAmount = greaterThan ? convertToFrontendAmountAsString(Number(greaterThan)) : undefined;
const lessThan = searchAdvancedFiltersForm?.[INPUT_IDS.LESS_THAN];
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

0 comments on commit 42e8486

Please sign in to comment.