diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f08227b71519..51b8f0e4e21f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -47,6 +47,8 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status', + SEARCH_ADVANCED_FILTERS_CURRENCY: 'search/filters/currency', + SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant', SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 40a1ebd817fa..66d727600ef4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -35,6 +35,7 @@ const SCREENS = { 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', + ADVANCED_FILTERS_CURRENCY_RHP: 'Search_Advanced_Filters_Currency_RHP', 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', diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index bfe4578afd0f..01a27ff5951a 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -3,18 +3,19 @@ import React, {useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; +import SelectableListItem from '@components/SelectionList/SelectableListItem'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; -function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList}: CurrencySelectionListProps) { +function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList, selectedCurrencies = [], canSelectMultiple = false}: CurrencySelectionListProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); const {sections, headerMessage} = useMemo(() => { const currencyOptions: CurrencyListItem[] = Object.entries(currencyList ?? {}).map(([currencyCode, currencyInfo]) => { - const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode; + const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode || selectedCurrencies.includes(currencyCode); return { currencyName: currencyInfo?.name ?? '', text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, @@ -28,6 +29,16 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, const filteredCurrencies = currencyOptions.filter((currencyOption) => searchRegex.test(currencyOption.text ?? '') || searchRegex.test(currencyOption.currencyName)); const isEmpty = searchValue.trim() && !filteredCurrencies.length; + if (canSelectMultiple) { + filteredCurrencies.sort((currencyA, currencyB) => { + if (currencyA.isSelected === currencyB.isSelected) { + return 0; + } + + return currencyA.isSelected ? -1 : 1; + }); + } + return { sections: isEmpty ? [] @@ -38,12 +49,12 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', }; - }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode]); + }, [currencyList, searchValue, canSelectMultiple, translate, initiallySelectedCurrencyCode, selectedCurrencies]); return ( ); } diff --git a/src/components/CurrencySelectionList/types.ts b/src/components/CurrencySelectionList/types.ts index eb7cf72d4e1e..9bfc31e10886 100644 --- a/src/components/CurrencySelectionList/types.ts +++ b/src/components/CurrencySelectionList/types.ts @@ -21,6 +21,12 @@ type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { /** Callback to fire when a currency is selected */ onSelect: (item: CurrencyListItem) => void; + + /** The array of selected currencies. This prop should be used when multiple currencies can be selected */ + selectedCurrencies?: string[]; + + /** Whether this is a multi-select list */ + canSelectMultiple?: boolean; }; export type {CurrencyListItem, CurrencySelectionListProps, CurrencySelectionListOnyxProps}; diff --git a/src/languages/en.ts b/src/languages/en.ts index 8cc1f0887e75..b984230b8194 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3621,6 +3621,7 @@ export default { after: (date?: string) => `After ${date ?? ''}`, }, status: 'Status', + currency: 'Currency', }, }, genericErrorPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index b659123c0632..74781c1fdfd9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3675,6 +3675,7 @@ export default { after: (date?: string) => `Después de ${date ?? ''}`, }, status: 'Estado', + currency: 'Divisa', }, }, genericErrorPage: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 407db0b1fdd5..d43e100d04a1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -514,6 +514,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchFiltersDatePage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require('../../../../pages/Search/SearchFiltersTypePage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require('../../../../pages/Search/SearchFiltersStatusPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: () => require('../../../../pages/Search/SearchFiltersCurrencyPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchFiltersDescriptionPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchFiltersMerchantPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchFiltersReportIDPage').default, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 758d471e2f7e..00edd4422b5b 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -42,6 +42,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SEARCH.REPORT_RHP, SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 663d7bf47d75..46720303b4cb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1017,6 +1017,7 @@ const config: LinkingOptions['config'] = { [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.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CURRENCY, [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, diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index a7548500de4a..ae2393941ac9 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -424,6 +424,9 @@ function buildQueryStringFromFilters(filterValues: Partial 0) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY}:${filterValue.join(',')}`; + } if (filterKey === INPUT_IDS.MERCHANT && filterValue) { return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT}:${filterValue as string}`; } @@ -497,7 +500,7 @@ function buildFilterString(filterName: string, queryFilters: QueryFilter[]) { 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 += `,${filterName}:${queryFilter.value}`; + filterValueString += `,${sanitizeString(queryFilter.value.toString())}`; } else { filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${queryFilter.value}`; } diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index fcd5190e0212..1ff77776d5ca 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -5,6 +5,7 @@ import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; import type {AdvancedFiltersKeys} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -36,9 +37,13 @@ function getFilterDisplayTitle(filters: Partial, fiel return dateValue; } - if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY && filters[fieldName]) { - const categories = filters[fieldName] ?? []; - return categories.join(', '); + if ((fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY) && filters[fieldName]) { + const filterArray = filters[fieldName] ?? []; + return filterArray.join(', '); + } + + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION) { + return filters[fieldName]; } // Todo Once all Advanced filters are implemented this line can be cleaned up. See: https://github.com/Expensify/App/issues/45026 @@ -73,6 +78,11 @@ function AdvancedSearchFilters() { description: 'common.date' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_DATE, }, + { + title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY, translate), + description: 'common.currency' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_CURRENCY, + }, { title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT, translate), description: 'common.merchant' as const, @@ -109,7 +119,7 @@ function AdvancedSearchFilters() { }; return ( - + {advancedFilters.map((item) => { const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route))); @@ -127,11 +137,11 @@ function AdvancedSearchFilters() { - + ); } diff --git a/src/pages/Search/SearchAdvancedFiltersPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage.tsx index 9d4864bce8eb..e24d2411226b 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage.tsx @@ -15,6 +15,7 @@ function SearchAdvancedFiltersPage() { testID={SearchAdvancedFiltersPage.displayName} shouldShowOfflineIndicatorInWideScreen offlineIndicatorStyle={styles.mtAuto} + includeSafeAreaPaddingBottom={false} > diff --git a/src/pages/Search/SearchFiltersCategoryPage.tsx b/src/pages/Search/SearchFiltersCategoryPage.tsx index 29a149482c81..8105bde2745c 100644 --- a/src/pages/Search/SearchFiltersCategoryPage.tsx +++ b/src/pages/Search/SearchFiltersCategoryPage.tsx @@ -126,6 +126,7 @@ function SearchFiltersCategoryPage() { testID={SearchFiltersCategoryPage.displayName} shouldShowOfflineIndicatorInWideScreen offlineIndicatorStyle={styles.mtAuto} + includeSafeAreaPaddingBottom={false} > - + (searchAdvancedFiltersForm?.currency ?? []); + + const handleOnSelectOption = (option: CurrencyListItem) => { + if (selectedCurrencies.includes(option.currencyCode)) { + setSelectedCurrencies(selectedCurrencies.filter((currency) => currency !== option.currencyCode)); + return; + } + + setSelectedCurrencies([option.currencyCode, ...selectedCurrencies]); + }; + + const handleOnSubmit = () => { + SearchActions.updateAdvancedFilters({...searchAdvancedFiltersForm, currency: selectedCurrencies}); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }; + + return ( + + {({didScreenTransitionEnd}) => ( + + + { + if (!didScreenTransitionEnd) { + return; + } + handleOnSelectOption(option); + }} + /> + + + )} + + ); +} + +SearchFiltersCurrencyPage.displayName = 'SearchFiltersCurrencyPage'; + +export default SearchFiltersCurrencyPage; diff --git a/src/pages/Search/SearchFiltersDatePage.tsx b/src/pages/Search/SearchFiltersDatePage.tsx index 3cc19a501198..4bc95aa21351 100644 --- a/src/pages/Search/SearchFiltersDatePage.tsx +++ b/src/pages/Search/SearchFiltersDatePage.tsx @@ -34,6 +34,7 @@ function SearchFiltersDatePage() { testID={SearchFiltersDatePage.displayName} shouldShowOfflineIndicatorInWideScreen offlineIndicatorStyle={styles.mtAuto} + includeSafeAreaPaddingBottom={false} > - + - + - + {({hovered}) => ( - + - - - {menuTitle} - - + + {menuTitle} +