From 1b3590e31a50dbd516213be0f8c717a25ece8bad Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 17 Oct 2024 09:29:44 -0600 Subject: [PATCH 1/2] Revert "[CP Staging] Revert "Support empty value for categories and tags"" --- src/CONST.ts | 1 + .../Search/SearchMultipleSelectionPicker.tsx | 16 ++++++++-- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/pages/Search/AdvancedSearchFilters.tsx | 32 ++++++++++++++++--- .../SearchFiltersCategoryPage.tsx | 17 +++++++--- .../SearchFiltersTagPage.tsx | 17 +++++++--- 7 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e5435ea5c440..a0612397934f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5678,6 +5678,7 @@ const CONST = { KEYWORD: 'keyword', IN: 'in', }, + EMPTY_VALUE: 'none', }, REFERRER: { diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 558b89715b61..d76f2e76ab02 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {OptionData} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; type SearchMultipleSelectionPickerItem = { @@ -28,6 +29,17 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedItems, setSelectedItems] = useState(initiallySelectedItems ?? []); + const sortOptionsWithEmptyValue = (a: SearchMultipleSelectionPickerItem, b: SearchMultipleSelectionPickerItem) => { + // Always show `No category` and `No tag` as the first option + if (a.value === CONST.SEARCH.EMPTY_VALUE) { + return -1; + } + if (b.value === CONST.SEARCH.EMPTY_VALUE) { + return 1; + } + return localeCompare(a.name, b.name); + }; + useEffect(() => { setSelectedItems(initiallySelectedItems ?? []); }, [initiallySelectedItems]); @@ -35,7 +47,7 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit const {sections, noResultsFound} = useMemo(() => { const selectedItemsSection = selectedItems .filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) - .sort((a, b) => localeCompare(a.name, b.name)) + .sort((a, b) => sortOptionsWithEmptyValue(a, b)) .map((item) => ({ text: item.name, keyForList: item.name, @@ -44,7 +56,7 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit })); const remainingItemsSection = items .filter((item) => selectedItems.some((selectedItem) => selectedItem.value === item.value) === false && item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) - .sort((a, b) => localeCompare(a.name, b.name)) + .sort((a, b) => sortOptionsWithEmptyValue(a, b)) .map((item) => ({ text: item.name, keyForList: item.name, diff --git a/src/languages/en.ts b/src/languages/en.ts index 24f0d990cd7a..d73015693e7a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4292,6 +4292,8 @@ const translations = { current: 'Current', past: 'Past', }, + noCategory: 'No category', + noTag: 'No tag', expenseType: 'Expense type', recentSearches: 'Recent searches', recentChats: 'Recent chats', diff --git a/src/languages/es.ts b/src/languages/es.ts index ff1c9238e847..a9d35a6f8228 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4338,6 +4338,8 @@ const translations = { current: 'Actual', past: 'Anterior', }, + noCategory: 'Sin categoría', + noTag: 'Sin etiqueta', expenseType: 'Tipo de gasto', recentSearches: 'Búsquedas recientes', recentChats: 'Chats recientes', diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 54b62a61724d..6e117a8baaab 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -139,6 +139,17 @@ function getFilterParticipantDisplayTitle(accountIDs: string[], personalDetails: .join(', '); } +const sortOptionsWithEmptyValue = (a: string, b: string) => { + // Always show `No category` and `No tag` as the first option + if (a === CONST.SEARCH.EMPTY_VALUE) { + return -1; + } + if (b === CONST.SEARCH.EMPTY_VALUE) { + return 1; + } + return localeCompare(a, b); +}; + function getFilterDisplayTitle(filters: Partial, 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 @@ -175,14 +186,27 @@ function getFilterDisplayTitle(filters: Partial, fiel return; } - if ( - (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) && - filters[fieldName] - ) { + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY && filters[fieldName]) { const filterArray = filters[fieldName] ?? []; return filterArray.sort(localeCompare).join(', '); } + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY && filters[fieldName]) { + const filterArray = filters[fieldName] ?? []; + return filterArray + .sort(sortOptionsWithEmptyValue) + .map((value) => (value === CONST.SEARCH.EMPTY_VALUE ? translate('search.noCategory') : value)) + .join(', '); + } + + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG && filters[fieldName]) { + const filterArray = filters[fieldName] ?? []; + return filterArray + .sort(sortOptionsWithEmptyValue) + .map((value) => (value === CONST.SEARCH.EMPTY_VALUE ? translate('search.noTag') : value)) + .join(', '); + } + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION) { return filters[fieldName]; } diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage.tsx index 0aa6cea05c0e..d92554d42453 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage.tsx @@ -9,6 +9,7 @@ import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as SearchActions from '@userActions/Search'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -17,19 +18,27 @@ function SearchFiltersCategoryPage() { const {translate} = useLocalize(); const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); - const selectedCategoriesItems = searchAdvancedFiltersForm?.category?.map((category) => ({name: category, value: category})); + const selectedCategoriesItems = searchAdvancedFiltersForm?.category?.map((category) => { + if (category === CONST.SEARCH.EMPTY_VALUE) { + return {name: translate('search.noCategory'), value: category}; + } + return {name: category, value: category}; + }); const policyID = searchAdvancedFiltersForm?.policyID ?? '-1'; const [allPolicyIDCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const singlePolicyCategories = allPolicyIDCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]; const categoryItems = useMemo(() => { + const items = [{name: translate('search.noCategory'), value: CONST.SEARCH.EMPTY_VALUE as string}]; if (!singlePolicyCategories) { const uniqueCategoryNames = new Set(); Object.values(allPolicyIDCategories ?? {}).map((policyCategories) => Object.values(policyCategories ?? {}).forEach((category) => uniqueCategoryNames.add(category.name))); - return Array.from(uniqueCategoryNames).map((categoryName) => ({name: categoryName, value: categoryName})); + items.push(...Array.from(uniqueCategoryNames).map((categoryName) => ({name: categoryName, value: categoryName}))); + } else { + items.push(...Object.values(singlePolicyCategories ?? {}).map((category) => ({name: category.name, value: category.name}))); } - return Object.values(singlePolicyCategories ?? {}).map((category) => ({name: category.name, value: category.name})); - }, [allPolicyIDCategories, singlePolicyCategories]); + return items; + }, [allPolicyIDCategories, singlePolicyCategories, translate]); const onSaveSelection = useCallback((values: string[]) => SearchActions.updateAdvancedFilters({category: values}), []); const safePaddingBottomStyle = useSafePaddingBottomStyle(); diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage.tsx index 107d100254cb..76e2ca45144f 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage.tsx @@ -9,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {getTagNamesFromTagsLists} from '@libs/PolicyUtils'; import * as SearchActions from '@userActions/Search'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PolicyTagLists} from '@src/types/onyx'; @@ -18,12 +19,18 @@ function SearchFiltersTagPage() { const {translate} = useLocalize(); const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); - const selectedTagsItems = searchAdvancedFiltersForm?.tag?.map((tag) => ({name: tag, value: tag})); + const selectedTagsItems = searchAdvancedFiltersForm?.tag?.map((tag) => { + if (tag === CONST.SEARCH.EMPTY_VALUE) { + return {name: translate('search.noTag'), value: tag}; + } + return {name: tag, value: tag}; + }); const policyID = searchAdvancedFiltersForm?.policyID ?? '-1'; const [allPoliciesTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const singlePolicyTagsList: PolicyTagLists | undefined = allPoliciesTagsLists?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]; const tagItems = useMemo(() => { + const items = [{name: translate('search.noTag'), value: CONST.SEARCH.EMPTY_VALUE as string}]; if (!singlePolicyTagsList) { const uniqueTagNames = new Set(); const tagListsUnpacked = Object.values(allPoliciesTagsLists ?? {}).filter((item) => !!item) as PolicyTagLists[]; @@ -33,10 +40,12 @@ function SearchFiltersTagPage() { }) .flat() .forEach((tag) => uniqueTagNames.add(tag)); - return Array.from(uniqueTagNames).map((tagName) => ({name: tagName, value: tagName})); + items.push(...Array.from(uniqueTagNames).map((tagName) => ({name: tagName, value: tagName}))); + } else { + items.push(...getTagNamesFromTagsLists(singlePolicyTagsList).map((name) => ({name, value: name}))); } - return getTagNamesFromTagsLists(singlePolicyTagsList).map((name) => ({name, value: name})); - }, [allPoliciesTagsLists, singlePolicyTagsList]); + return items; + }, [allPoliciesTagsLists, singlePolicyTagsList, translate]); const updateTagFilter = useCallback((values: string[]) => SearchActions.updateAdvancedFilters({tag: values}), []); From bc2514dc5f91b97e96b4f456c410bd2be9410405 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 17 Oct 2024 10:43:48 -0600 Subject: [PATCH 2/2] push empty values to categories and tags --- src/libs/SearchUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index ef9f237bd551..9e4a7579bca6 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -704,14 +704,16 @@ function buildFilterFormValuesFromQuery( .filter((item) => !!item) .map((tagList) => getTagNamesFromTagsLists(tagList ?? {})) .flat(); + tags.push(CONST.SEARCH.EMPTY_VALUE); filtersForm[filterKey] = filters[filterKey]?.map((tag) => tag.value.toString()).filter((name) => tags.includes(name)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { const categories = policyID ? Object.values(policyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}).map((category) => category.name) : Object.values(policyCategories ?? {}) - .map((xd) => Object.values(xd ?? {}).map((category) => category.name)) + .map((item) => Object.values(item ?? {}).map((category) => category.name)) .flat(); + categories.push(CONST.SEARCH.EMPTY_VALUE); filtersForm[filterKey] = filters[filterKey]?.map((category) => category.value.toString()).filter((name) => categories.includes(name)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) {