diff --git a/src/CONST.ts b/src/CONST.ts index 0f0105fdc358..e2a1e79ccbb3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5339,6 +5339,11 @@ const CONST = { DONE: 'done', PAID: 'paid', }, + CHAT_STATUS: { + UNREAD: 'unread', + PINNED: 'pinned', + DRAFT: 'draft', + }, BULK_ACTION_TYPES: { EXPORT: 'export', HOLD: 'hold', @@ -5427,6 +5432,7 @@ const CONST = { KEYWORD: 'keyword', IN: 'in', HAS: 'has', + IS: 'is', }, }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 89023063ad8f..9e9bca2a4037 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -54,6 +54,7 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_TO: 'search/filters/to', SEARCH_ADVANCED_FILTERS_IN: 'search/filters/in', SEARCH_ADVANCED_FILTERS_HAS: 'search/filters/has', + SEARCH_ADVANCED_FILTERS_IS: 'search/filters/is', SEARCH_REPORT: { route: 'search/view/:reportID', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index db790dd389c3..b66627153ea8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -48,6 +48,7 @@ const SCREENS = { ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP', ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', ADVANCED_FILTERS_HAS_RHP: 'Search_Advanced_Filters_Has_RHP', + ADVANCED_FILTERS_IS_RHP: 'Search_Advanced_Filters_Is_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index d8d917f7730f..c2902dc3d8a5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3882,6 +3882,10 @@ export default { currency: 'Currency', has: 'Has', link: 'Link', + is: 'Is', + pinned: 'Pinned', + unread: 'Unread', + draft: 'Draft', amount: { lessThan: (amount?: string) => `Less than ${amount ?? ''}`, greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 14b461ffa064..9ce98e12b135 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3933,6 +3933,10 @@ export default { currency: 'Divisa', has: 'Tiene', link: 'Enlace', + is: 'Está', + pinned: 'Fijado', + unread: 'No leído', + draft: 'Borrador', amount: { lessThan: (amount?: string) => `Menos de ${amount ?? ''}`, greaterThan: (amount?: string) => `Más que ${amount ?? ''}`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c2a30f20ed56..e103e81c47d3 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -541,6 +541,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_HAS_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersHasPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_IS_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersIsPage').default, }); const RestrictedActionModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index cf45126bfc04..a0e1b9a25d35 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1070,6 +1070,7 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TO, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IN, [SCREENS.SEARCH.ADVANCED_FILTERS_HAS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_HAS, + [SCREENS.SEARCH.ADVANCED_FILTERS_IS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IS, }, }, [SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f9acb67a1efb..a2b1f75c3a2d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -309,7 +309,7 @@ function getTagNamesFromTagsLists(policyTagLists: PolicyTagLists): string[] { const uniqueTagNames = new Set(); for (const policyTagList of Object.values(policyTagLists ?? {})) { - for (const tag of Object.values(policyTagList.tags)) { + for (const tag of Object.values(policyTagList.tags ?? {})) { uniqueTagNames.add(getCleanedTagName(tag.name)); } } diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index a93e3fae8551..4c6c382d6224 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -201,7 +201,8 @@ function peg$parse(input, options) { var peg$c23 = "sortOrder"; var peg$c24 = "policyID"; var peg$c25 = "has"; - var peg$c26 = "\""; + var peg$c26 = "is"; + var peg$c27 = "\""; var peg$r0 = /^[:=]/; var peg$r1 = /^[^"\r\n]/; @@ -235,11 +236,12 @@ function peg$parse(input, options) { var peg$e24 = peg$literalExpectation("sortOrder", false); var peg$e25 = peg$literalExpectation("policyID", false); var peg$e26 = peg$literalExpectation("has", false); - var peg$e27 = peg$literalExpectation("\"", false); - var peg$e28 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e29 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); - var peg$e30 = peg$otherExpectation("whitespace"); - var peg$e31 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e27 = peg$literalExpectation("is", false); + var peg$e28 = peg$literalExpectation("\"", false); + var peg$e29 = peg$classExpectation(["\"", "\r", "\n"], true, false); + var peg$e30 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); + var peg$e31 = peg$otherExpectation("whitespace"); + var peg$e32 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); var peg$f0 = function(filters) { const withDefaults = applyDefaults(filters); @@ -314,10 +316,11 @@ function peg$parse(input, options) { var peg$f27 = function() { return "sortOrder"; }; var peg$f28 = function() { return "policyID"; }; var peg$f29 = function() { return "has"; }; - var peg$f30 = function(parts) { return parts.join(''); }; - var peg$f31 = function(chars) { return chars.join(''); }; + var peg$f30 = function() { return "is"; }; + var peg$f31 = function(parts) { return parts.join(''); }; var peg$f32 = function(chars) { return chars.join(''); }; - var peg$f33 = function() { return "and"; }; + var peg$f33 = function(chars) { return chars.join(''); }; + var peg$f34 = function() { return "and"; }; var peg$currPos = options.peg$currPos | 0; var peg$savedPos = peg$currPos; var peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -954,6 +957,21 @@ function peg$parse(input, options) { s1 = peg$f29(); } s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c26) { + s1 = peg$c26; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e27); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f30(); + } + s0 = s1; + } } } } @@ -1000,7 +1018,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f30(s1); + s1 = peg$f31(s1); } s0 = s1; @@ -1012,11 +1030,11 @@ function peg$parse(input, options) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c26; + s1 = peg$c27; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } + if (peg$silentFails === 0) { peg$fail(peg$e28); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1025,7 +1043,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e29); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -1034,19 +1052,19 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e29); } } } if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c26; + s3 = peg$c27; peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } + if (peg$silentFails === 0) { peg$fail(peg$e28); } } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f31(s2); + s0 = peg$f32(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1069,7 +1087,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e30); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -1079,7 +1097,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e30); } } } } else { @@ -1087,7 +1105,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f32(s1); + s1 = peg$f33(s1); } s0 = s1; @@ -1100,7 +1118,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f33(); + s1 = peg$f34(); s0 = s1; return s0; @@ -1116,7 +1134,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e31); } + if (peg$silentFails === 0) { peg$fail(peg$e32); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -1125,12 +1143,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e31); } + if (peg$silentFails === 0) { peg$fail(peg$e32); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e30); } + if (peg$silentFails === 0) { peg$fail(peg$e31); } return s0; } diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 105a8a62bc39..805ee3e668de 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -135,6 +135,7 @@ key / "sortOrder" { return "sortOrder"; } / "policyID" { return "policyID"; } / "has" { return "has"; } + / "is" { return "is"; } identifier = parts:(quotedString / alphanumeric)+ { return parts.join(''); } diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 790097597e6a..e0a12e967bfa 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -430,6 +430,18 @@ function getChatFiltersTranslationKey(has: ValueOf): TranslationPaths { + // eslint-disable-next-line default-case + switch (chatStatus) { + case CONST.SEARCH.CHAT_STATUS.PINNED: + return 'search.filters.pinned'; + case CONST.SEARCH.CHAT_STATUS.UNREAD: + return 'search.filters.unread'; + case CONST.SEARCH.CHAT_STATUS.DRAFT: + return 'search.filters.draft'; + } +} + /** * Given object with chosen search filters builds correct query string from them */ @@ -458,7 +470,8 @@ function buildQueryStringFromFilters(filterValues: Partial 0 ) { @@ -605,4 +618,5 @@ export { isCannedSearchQuery, getExpenseTypeTranslationKey, getChatFiltersTranslationKey, + getChatStatusTranslationKey, }; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 28567cfa8fe5..7775825f33de 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -108,13 +108,18 @@ const baseFilterConfig = { description: 'common.in' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_IN, }, + is: { + getTitle: getFilterIsDisplayTitle, + description: 'search.filters.is' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_IS, + }, }; const typeFiltersKeys: Record>> = { [CONST.SEARCH.DATA_TYPES.EXPENSE]: ['date', 'currency', 'merchant', 'description', 'reportID', 'amount', 'category', 'keyword', 'taxRate', 'expenseType', 'tag', 'from', 'to', 'cardID'], [CONST.SEARCH.DATA_TYPES.INVOICE]: ['date', 'currency', 'merchant', 'description', 'reportID', 'amount', 'category', 'keyword', 'taxRate', 'tag', 'from', 'to', 'cardID'], [CONST.SEARCH.DATA_TYPES.TRIP]: ['date', 'currency', 'merchant', 'description', 'reportID', 'amount', 'category', 'keyword', 'taxRate', 'tag', 'from', 'to', 'cardID'], - [CONST.SEARCH.DATA_TYPES.CHAT]: ['date', 'keyword', 'from', 'has', 'in'], + [CONST.SEARCH.DATA_TYPES.CHAT]: ['date', 'keyword', 'from', 'has', 'in', 'is'], }; function getFilterCardDisplayTitle(filters: Partial, cards: CardList) { @@ -232,6 +237,16 @@ function getFilterHasDisplayTitle(filters: Partial, t : undefined; } +function getFilterIsDisplayTitle(filters: Partial, translate: LocaleContextProps['translate']) { + const filterValue = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.IS]; + return filterValue + ? Object.values(CONST.SEARCH.CHAT_STATUS) + .filter((chatStatus) => filterValue.includes(chatStatus)) + .map((chatStatus) => translate(SearchUtils.getChatStatusTranslationKey(chatStatus))) + .join(', ') + : undefined; +} + function AdvancedSearchFilters() { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -278,7 +293,7 @@ function AdvancedSearchFilters() { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, cardList); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, taxRates); - } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS) { + } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IS) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, translate); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters[key] ?? [], personalDetails); diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage.tsx index 65d474b41905..941602f8efe8 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage.tsx @@ -65,6 +65,6 @@ function SearchFiltersExpenseTypePage() { ); } -SearchFiltersExpenseTypePage.displayName = 'SearchFiltersCategoryPage'; +SearchFiltersExpenseTypePage.displayName = 'SearchFiltersExpenseTypePage'; export default SearchFiltersExpenseTypePage; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersIsPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersIsPage.tsx new file mode 100644 index 000000000000..d2431776eb44 --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersIsPage.tsx @@ -0,0 +1,66 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchMultipleSelectionPicker from '@components/Search/SearchMultipleSelectionPicker'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {getChatStatusTranslationKey} from '@libs/SearchUtils'; +import * as SearchActions from '@userActions/Search'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +function SearchFiltersIsPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const selectedChatStatuses = searchAdvancedFiltersForm?.is?.map((chatStatus) => { + const chatStatusName = translate(getChatStatusTranslationKey(chatStatus as ValueOf)); + return {name: chatStatusName, value: chatStatus}; + }); + const allChatStatuses = Object.values(CONST.SEARCH.CHAT_STATUS); + + const chatStatusItems = useMemo(() => { + return allChatStatuses.map((chatStatus) => { + const chatStatusName = translate(getChatStatusTranslationKey(chatStatus)); + return {name: chatStatusName, value: chatStatus}; + }); + }, [allChatStatuses, translate]); + + const updateChatIsFilter = useCallback((values: string[]) => SearchActions.updateAdvancedFilters({is: values}), []); + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + + + + ); +} + +SearchFiltersIsPage.displayName = 'SearchFiltersIsPage'; + +export default SearchFiltersIsPage; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage.tsx index 713e9f5110b2..a1ad32db6308 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage.tsx @@ -67,6 +67,6 @@ function SearchFiltersTaxRatePage() { ); } -SearchFiltersTaxRatePage.displayName = 'SearchFiltersCategoryPage'; +SearchFiltersTaxRatePage.displayName = 'SearchFiltersTaxRatePage'; export default SearchFiltersTaxRatePage; diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 7645d86c5103..7f16c194b8d1 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -22,6 +22,7 @@ const FILTER_KEYS = { TO: 'to', IN: 'in', HAS: 'has', + IS: 'is', } as const; type InputID = ValueOf; @@ -49,6 +50,7 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.TO]: string[]; [FILTER_KEYS.IN]: string[]; [FILTER_KEYS.HAS]: string[]; + [FILTER_KEYS.IS]: string[]; } >;