From 0f63c8a1867407a9cc474866f6f18f06dd4328b1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 28 Feb 2024 14:55:53 +0530 Subject: [PATCH 01/28] fix: IOU - Disabled tag is greyed in list but disabled category is shown bold in list. Signed-off-by: Krishna Gupta --- .../CategoryPicker/categoryPickerPropTypes.js | 3 +++ src/components/CategoryPicker/index.js | 21 ++++++++++++++----- src/components/TagPicker/index.js | 5 +++-- .../request/step/IOURequestStepCategory.js | 1 + 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/CategoryPicker/categoryPickerPropTypes.js b/src/components/CategoryPicker/categoryPickerPropTypes.js index 0bc116bf45cc..a1cbabd4be40 100644 --- a/src/components/CategoryPicker/categoryPickerPropTypes.js +++ b/src/components/CategoryPicker/categoryPickerPropTypes.js @@ -18,6 +18,9 @@ const propTypes = { /** Callback to fire when a category is pressed */ onSubmit: PropTypes.func.isRequired, + + /** Should show the selected option that is disabled? */ + shouldShowDisabledAndSelectedOption: PropTypes.bool, }; const defaultProps = { diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index 2374fc9e5d0c..1887d46dc505 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -11,7 +11,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, propTypes} from './categoryPickerPropTypes'; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}) { +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit, shouldShowDisabledAndSelectedOption}) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -20,15 +20,26 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return []; } + const isSelectedCateoryEnabled = _.some(policyCategories, (category) => category.name === selectedCategory && category.enabled); + return [ { name: selectedCategory, - enabled: true, + enabled: isSelectedCateoryEnabled, accountID: null, isSelected: true, }, ]; - }, [selectedCategory]); + }, [selectedCategory, policyCategories]); + + const enabledCategories = useMemo(() => { + if (!shouldShowDisabledAndSelectedOption) { + return policyCategories; + } + const selectedNames = _.map(selectedOptions, (s) => s.name); + const catergories = [...selectedOptions, ..._.filter(policyCategories, (category) => category.enabled && !selectedNames.includes(category.name))]; + return catergories; + }, [selectedOptions, policyCategories, shouldShowDisabledAndSelectedOption]); const [sections, headerMessage, policyCategoriesCount, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = _.filter(policyRecentlyUsedCategories, (p) => !_.isEmpty(p)); @@ -42,7 +53,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC false, false, true, - policyCategories, + enabledCategories, validPolicyRecentlyUsedCategories, false, ); @@ -53,7 +64,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC const showInput = !isCategoriesCountBelowThreshold; return [categoryOptions, header, policiesCount, showInput]; - }, [policyCategories, policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions]); + }, [policyCategories, policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, enabledCategories]); const selectedOptionKey = useMemo( () => lodashGet(_.filter(lodashGet(sections, '[0].data', []), (category) => category.searchText === selectedCategory)[0], 'keyForList'), diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 341ea9cddae9..557a8ad918e1 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -29,15 +29,16 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa if (!selectedTag) { return []; } + const selectedTagInList = _.some(policyTagList.tags, (policyTag) => policyTag.name === selectedTag && policyTag.enabled); return [ { name: selectedTag, - enabled: true, + enabled: selectedTagInList, accountID: null, }, ]; - }, [selectedTag]); + }, [selectedTag, policyTagList.tags]); const enabledTags = useMemo(() => { if (!shouldShowDisabledAndSelectedOption) { diff --git a/src/pages/iou/request/step/IOURequestStepCategory.js b/src/pages/iou/request/step/IOURequestStepCategory.js index 3e0feec02854..0c79aa12896b 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.js +++ b/src/pages/iou/request/step/IOURequestStepCategory.js @@ -120,6 +120,7 @@ function IOURequestStepCategory({ selectedCategory={transactionCategory} policyID={report.policyID} onSubmit={updateCategory} + shouldShowDisabledAndSelectedOption={isEditing} /> ); From 22a6f208a0f080195191d85deb40f4e68c8e98e8 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 09:23:22 +0530 Subject: [PATCH 02/28] resolve conflicts. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker/categoryPickerPropTypes.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/components/CategoryPicker/categoryPickerPropTypes.js diff --git a/src/components/CategoryPicker/categoryPickerPropTypes.js b/src/components/CategoryPicker/categoryPickerPropTypes.js deleted file mode 100644 index e69de29bb2d1..000000000000 From 061cdd1771362037f8363050dd1a34545b38be4b Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 12:29:41 +0530 Subject: [PATCH 03/28] apply changes to new CategoryPicker.tsx Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 3033bf118e8f..9213fbdfe4b9 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -34,15 +34,17 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return []; } + const selectedCategoryInList = Object.values(policyCategories ?? {}).some((category) => category.name === selectedCategory && category.enabled); + return [ { name: selectedCategory, - enabled: true, + enabled: selectedCategoryInList, accountID: null, isSelected: true, }, ]; - }, [selectedCategory]); + }, [selectedCategory, policyCategories]); const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p)); From d3df9e9f91f11687af6c8a8dc3edcfa8c453ac7d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 9 Mar 2024 13:58:36 +0530 Subject: [PATCH 04/28] fix: disabled seleted category not shown in list when searching. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fd803a508b4a..8176bb81dd88 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -956,10 +956,11 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const enabledCategories = [...selectedOptions, ...Object.values(sortedCategories).filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; + const numberOfCategories = enabledCategories.length; const categorySections: CategoryTreeSection[] = []; - const numberOfCategories = enabledCategories.length; let indexOffset = 0; @@ -989,17 +990,13 @@ function getCategoryListSections( return categorySections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledAndSelectedCategories = [...selectedOptions, ...sortedCategories.filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; - const numberOfVisibleCategories = enabledAndSelectedCategories.length; - - if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) { + if (numberOfCategories < CONST.CATEGORY_LIST_THRESHOLD) { categorySections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(enabledAndSelectedCategories), + data: getCategoryOptionTree(enabledCategories), }); return categorySections; From d78eebd99a6bfbd512cf84ec2a69c7e5c46ecb83 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 9 Mar 2024 14:00:27 +0530 Subject: [PATCH 05/28] revert all previous changes. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 6 ++---- src/components/TagPicker/index.js | 6 ++---- src/pages/iou/request/step/IOURequestStepCategory.js | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 9213fbdfe4b9..3033bf118e8f 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -34,17 +34,15 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return []; } - const selectedCategoryInList = Object.values(policyCategories ?? {}).some((category) => category.name === selectedCategory && category.enabled); - return [ { name: selectedCategory, - enabled: selectedCategoryInList, + enabled: true, accountID: null, isSelected: true, }, ]; - }, [selectedCategory, policyCategories]); + }, [selectedCategory]); const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p)); diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 557a8ad918e1..38e863730353 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -29,16 +29,14 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa if (!selectedTag) { return []; } - const selectedTagInList = _.some(policyTagList.tags, (policyTag) => policyTag.name === selectedTag && policyTag.enabled); - return [ { name: selectedTag, - enabled: selectedTagInList, + enabled: true, accountID: null, }, ]; - }, [selectedTag, policyTagList.tags]); + }, [selectedTag]); const enabledTags = useMemo(() => { if (!shouldShowDisabledAndSelectedOption) { diff --git a/src/pages/iou/request/step/IOURequestStepCategory.js b/src/pages/iou/request/step/IOURequestStepCategory.js index 3a85c65f3441..1945edbc24c4 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.js +++ b/src/pages/iou/request/step/IOURequestStepCategory.js @@ -121,7 +121,6 @@ function IOURequestStepCategory({ selectedCategory={transactionCategory} policyID={report.policyID} onSubmit={updateCategory} - shouldShowDisabledAndSelectedOption={isEditing} /> ); From 0a3cd609e8b87742aa73ca953de849c4391c8521 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 9 Mar 2024 14:00:58 +0530 Subject: [PATCH 06/28] minor spacing fix Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 38e863730353..341ea9cddae9 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -29,6 +29,7 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa if (!selectedTag) { return []; } + return [ { name: selectedTag, From a4485ff37edf58c018765deb76cf1b85214f74ac Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 11 Mar 2024 15:57:16 +0530 Subject: [PATCH 07/28] fix jest tests fails. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 14 +++++++++++--- tests/unit/OptionsListUtilsTest.js | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 03aa8f952065..a56e4afae124 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -956,21 +956,29 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); + const numberOfCategories = sortedCategories.length; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const enabledCategories = [...selectedOptions, ...Object.values(sortedCategories).filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; - const numberOfCategories = enabledCategories.length; + const enabledAndSelectedCategoriesLength = enabledCategories.length; const categorySections: CategoryTreeSection[] = []; let indexOffset = 0; if (numberOfCategories === 0 && selectedOptions.length > 0) { + const selectedTagOptions = selectedOptions.map((option) => ({ + name: option.name, + // Should be marked as enabled to be able to be de-selected + enabled: true, + isSelected: true, + })); + categorySections.push({ // "Selected" section title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(selectedTagOptions, true), }); return categorySections; @@ -990,7 +998,7 @@ function getCategoryListSections( return categorySections; } - if (numberOfCategories < CONST.CATEGORY_LIST_THRESHOLD) { + if (enabledAndSelectedCategoriesLength < CONST.CATEGORY_LIST_THRESHOLD) { categorySections.push({ // "All" section when items amount less than the threshold title: '', diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 7244b7830a29..1eed8d922036 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1014,7 +1014,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: false, + isSelected: true, }, ], }, From e0918c80720e91af5a8cdacd659f57f3c5cc0ed7 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 11 Mar 2024 16:01:56 +0530 Subject: [PATCH 08/28] fix const names. Signed-off-by: Krishna Gupta --- Gemfile.lock | 16 +++++++++++----- src/libs/OptionsListUtils.ts | 24 +++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index beb2c1762936..7cc425fe6323 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,11 +3,12 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.8) + activesupport (6.1.7.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -80,7 +81,8 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) @@ -187,11 +189,11 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.47.0) + google-cloud-storage (1.37.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -260,6 +262,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) unicode-display_width (2.5.0) word_wrap (1.0.0) xcodeproj (1.23.0) @@ -273,6 +278,7 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) + zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -292,4 +298,4 @@ RUBY VERSION ruby 2.6.10p210 BUNDLED WITH - 2.4.19 + 2.4.22 diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a56e4afae124..042be402678a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -956,36 +956,30 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const numberOfCategories = sortedCategories.length; + const enabledCategoriesLength = Object.values(sortedCategories).filter((category) => category.enabled).length; + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledCategories = [...selectedOptions, ...Object.values(sortedCategories).filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; - const enabledAndSelectedCategoriesLength = enabledCategories.length; + const enabledAndSelectedCategories = [...selectedOptions, ...Object.values(sortedCategories).filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; + const enabledAndSelectedCategoriesLength = enabledAndSelectedCategories.length; const categorySections: CategoryTreeSection[] = []; let indexOffset = 0; - if (numberOfCategories === 0 && selectedOptions.length > 0) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to be de-selected - enabled: true, - isSelected: true, - })); - + if (enabledCategoriesLength === 0 && selectedOptions.length > 0) { categorySections.push({ // "Selected" section title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(selectedTagOptions, true), + data: getCategoryOptionTree(enabledAndSelectedCategories, true), }); return categorySections; } if (searchInputValue) { - const searchCategories = enabledCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + const searchCategories = enabledAndSelectedCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); categorySections.push({ // "Search" section @@ -1004,7 +998,7 @@ function getCategoryListSections( title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(enabledCategories), + data: getCategoryOptionTree(enabledAndSelectedCategories), }); return categorySections; @@ -1043,7 +1037,7 @@ function getCategoryListSections( indexOffset += filteredRecentlyUsedCategories.length; } - const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); + const filteredCategories = enabledAndSelectedCategories.filter((category) => !selectedOptionNames.includes(category.name)); categorySections.push({ // "All" section when items amount more than the threshold From dd517366675222f699f879e630ea1fec82fb2707 Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 11 Mar 2024 16:04:02 +0530 Subject: [PATCH 09/28] Update Gemfile.lock --- Gemfile.lock | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7cc425fe6323..e276bcacbbd7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,11 @@ -GEM - remote: https://rubygems.org/ specs: CFPropertyList (3.0.6) rexml - activesupport (6.1.7.7) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -81,8 +78,7 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) @@ -189,11 +185,11 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.37.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -262,9 +258,6 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.9.1) unicode-display_width (2.5.0) word_wrap (1.0.0) xcodeproj (1.23.0) @@ -278,7 +271,6 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -286,16 +278,14 @@ PLATFORMS universal-darwin-20 x86_64-darwin-19 x86_64-linux - DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) cocoapods (~> 1.13) fastlane (~> 2) fastlane-plugin-aws_s3 xcpretty (~> 0) - RUBY VERSION ruby 2.6.10p210 BUNDLED WITH - 2.4.22 + 2.4.19 From d8f81d81c9dc9f15a78a675664799b90a4021f87 Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 11 Mar 2024 16:05:19 +0530 Subject: [PATCH 10/28] Update Gemfile.lock --- Gemfile.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e276bcacbbd7..bf34eda0dac4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,4 +1,6 @@ - specs: +GEM + remote: https://rubygems.org/ +specs: CFPropertyList (3.0.6) rexml activesupport (7.0.8) @@ -278,12 +280,14 @@ PLATFORMS universal-darwin-20 x86_64-darwin-19 x86_64-linux + DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) cocoapods (~> 1.13) fastlane (~> 2) fastlane-plugin-aws_s3 xcpretty (~> 0) + RUBY VERSION ruby 2.6.10p210 From 9f9669d52286eefb4f815b7ad56c7a01b65442ad Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 11 Mar 2024 16:06:13 +0530 Subject: [PATCH 11/28] Update Gemfile.lock --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf34eda0dac4..beb2c1762936 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ -GEM +GEM remote: https://rubygems.org/ -specs: + specs: CFPropertyList (3.0.6) rexml activesupport (7.0.8) From cdd87ddb1642d60c2cf75e4241b3c3ffb423df8d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 11 Mar 2024 16:13:14 +0530 Subject: [PATCH 12/28] fix: jest tests. Signed-off-by: Krishna Gupta --- tests/unit/OptionsListUtilsTest.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 1eed8d922036..c3c84cdc2c83 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -689,6 +689,7 @@ describe('OptionsListUtils', () => { { name: 'Medical', enabled: true, + isSelected: true, }, ]; const smallCategoriesList = { @@ -845,7 +846,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: false, + isSelected: true, }, ], }, From 6262dcdafe8099d6d0cae9981ff1e78b29b127c4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 12:00:07 +0530 Subject: [PATCH 13/28] revert all changes. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 214 ++++++++++++------------- tests/unit/OptionsListUtilsTest.js | 249 ++++++++++++++++++++++++++--- 2 files changed, 329 insertions(+), 134 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 042be402678a..7e4082bff481 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,6 +20,7 @@ import type { PolicyCategories, PolicyTag, PolicyTagList, + PolicyTags, Report, ReportAction, ReportActions, @@ -31,6 +33,7 @@ import type { import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -53,12 +56,6 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type Tag = { - enabled: boolean; - name: string; - accountID: number | null; -}; - type Option = Partial; /** @@ -86,7 +83,6 @@ type PayeePersonalDetails = { type CategorySectionBase = { title: string | undefined; shouldShow: boolean; - indexOffset: number; }; type CategorySection = CategorySectionBase & { @@ -130,7 +126,7 @@ type GetOptionsConfig = { categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; - tags?: Record; + tags?: PolicyTags | Array; recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; @@ -154,7 +150,6 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; - newIndexOffset: number; }; type GetOptions = { recentReports: ReportUtils.OptionData[]; @@ -247,17 +242,6 @@ Onyx.connect({ }, }); -const policyExpenseReports: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report, key) => { - if (!ReportUtils.isPolicyExpenseChat(report)) { - return; - } - policyExpenseReports[key] = report; - }, -}); - let allTransactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -480,7 +464,7 @@ function getSearchText( /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry, transactions: OnyxCollection = allTransactions): OnyxCommon.Errors { +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce( @@ -492,7 +476,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; - const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); } @@ -520,7 +504,8 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< */ function getLastActorDisplayName(lastActorDetails: Partial | null, hasMultipleParticipants: boolean) { return hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID - ? lastActorDetails.firstName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) + ? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) : ''; } @@ -568,6 +553,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails ReportUtils.isChatReport(report), null, true, + lastReportAction, ); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); @@ -732,16 +718,45 @@ function createOption( return result; } +/** + * Get the option for a given report. + */ +function getReportOption(participant: Participant): ReportUtils.OptionData { + const report = ReportUtils.getReport(participant.reportID); + + const option = createOption( + report?.visibleChatMemberAccountIDs ?? [], + allPersonalDetails ?? {}, + !isEmptyObject(report) ? report : null, + {}, + { + showChatPreviewLine: false, + forcePolicyNamePreview: false, + }, + ); + + // Update text & alternateText because createOption returns workspace name only if report is owned by the user + if (option.isSelfDM) { + option.alternateText = Localize.translateLocal('reportActionsView.yourSpace'); + } else { + option.text = ReportUtils.getPolicyName(report); + option.alternateText = Localize.translateLocal('workspace.common.workspace'); + } + option.selected = participant.selected; + option.isSelected = participant.selected; + return option; +} + /** * Get the option for a policy expense report. */ -function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { - const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; +function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData { + const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReport(participant.reportID) : null; const option = createOption( expenseReport?.visibleChatMemberAccountIDs ?? [], allPersonalDetails ?? {}, - expenseReport ?? null, + !isEmptyObject(expenseReport) ? expenseReport : null, {}, { showChatPreviewLine: false, @@ -752,8 +767,8 @@ function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { // Update text & alternateText because createOption returns workspace name only if report is owned by the user option.text = ReportUtils.getPolicyName(expenseReport); option.alternateText = Localize.translateLocal('workspace.common.workspace'); - option.selected = report.selected; - option.isSelected = report.selected; + option.selected = participant.selected; + option.isSelected = participant.selected; return option; } @@ -861,7 +876,7 @@ function sortCategories(categories: Record): Category[] { if (name) { const categoryObject: Category = { name, - enabled: categories[name].enabled ?? false, + enabled: categories[name]?.enabled ?? false, }; acc.push(categoryObject); @@ -882,16 +897,11 @@ function sortCategories(categories: Record): Category[] { /** * Sorts tags alphabetically by name. */ -function sortTags(tags: Record | Tag[]) { - let sortedTags; - - if (Array.isArray(tags)) { - sortedTags = tags.sort((a, b) => localeCompare(a.name, b.name)); - } else { - sortedTags = Object.values(tags).sort((a, b) => localeCompare(a.name, b.name)); - } +function sortTags(tags: Record | Array) { + const sortedTags = Array.isArray(tags) ? tags : Object.values(tags); - return sortedTags; + // Use lodash's sortBy to ensure consistency with oldDot. + return lodashSortBy(sortedTags, 'name', localeCompare); } /** @@ -902,7 +912,7 @@ function sortTags(tags: Record | Tag[]) { * @param options[].name - a name of an option * @param [isOneLine] - a flag to determine if text should be one line */ -function getCategoryOptionTree(options: Record | Category[], isOneLine = false): OptionTree[] { +function getCategoryOptionTree(options: Record | Category[], isOneLine = false, selectedOptionsName: string[] = []): OptionTree[] { const optionCollection = new Map(); Object.values(options).forEach((option) => { if (isOneLine) { @@ -937,7 +947,7 @@ function getCategoryOptionTree(options: Record | Category[], i searchText, tooltipText: optionName, isDisabled: isChild ? !option.enabled : true, - isSelected: !!option.isSelected, + isSelected: isChild ? !!option.isSelected : selectedOptionsName.includes(searchText), }); }); }); @@ -956,64 +966,66 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const enabledCategoriesLength = Object.values(sortedCategories).filter((category) => category.enabled).length; - - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledAndSelectedCategories = [...selectedOptions, ...Object.values(sortedCategories).filter((category) => category.enabled && !selectedOptionNames.includes(category.name))]; - const enabledAndSelectedCategoriesLength = enabledAndSelectedCategories.length; + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); const categorySections: CategoryTreeSection[] = []; + const numberOfEnabledCategories = enabledCategories.length; - let indexOffset = 0; - - if (enabledCategoriesLength === 0 && selectedOptions.length > 0) { + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { categorySections.push({ // "Selected" section title: '', shouldShow: false, - indexOffset, - data: getCategoryOptionTree(enabledAndSelectedCategories, true), + data: getCategoryOptionTree(selectedOptions, true), }); return categorySections; } if (searchInputValue) { - const searchCategories = enabledAndSelectedCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + const searchCategories: Category[] = []; + + enabledCategories.forEach((category) => { + if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { + return; + } + searchCategories.push({ + ...category, + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), + }); + }); categorySections.push({ // "Search" section title: '', shouldShow: true, - indexOffset, data: getCategoryOptionTree(searchCategories, true), }); return categorySections; } - if (enabledAndSelectedCategoriesLength < CONST.CATEGORY_LIST_THRESHOLD) { + if (selectedOptions.length > 0) { categorySections.push({ - // "All" section when items amount less than the threshold + // "Selected" section title: '', shouldShow: false, - indexOffset, - data: getCategoryOptionTree(enabledAndSelectedCategories), + data: getCategoryOptionTree(selectedOptions, true), }); - - return categorySections; } - if (selectedOptions.length > 0) { + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); + + if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { categorySections.push({ - // "Selected" section + // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), }); - indexOffset += selectedOptions.length; + return categorySections; } const filteredRecentlyUsedCategories = recentlyUsedCategories @@ -1030,21 +1042,15 @@ function getCategoryListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getCategoryOptionTree(cutRecentlyUsedCategories, true), }); - - indexOffset += filteredRecentlyUsedCategories.length; } - const filteredCategories = enabledAndSelectedCategories.filter((category) => !selectedOptionNames.includes(category.name)); - categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, - data: getCategoryOptionTree(filteredCategories), + data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), }); return categorySections; @@ -1055,7 +1061,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Category[]): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1072,13 +1078,18 @@ function getTagsOptions(tags: Category[]): Option[] { /** * Build the section list for tags */ -function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { +function getTagListSections( + tags: Array, + recentlyUsedTags: string[], + selectedOptions: SelectedTagOption[], + searchInputValue: string, + maxRecentReportsToShow: number, +) { const tagSections = []; - const sortedTags = sortTags(tags); + const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; const numberOfTags = enabledTags.length; - let indexOffset = 0; // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { @@ -1091,7 +1102,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Selected" section title: '', shouldShow: false, - indexOffset, data: getTagsOptions(selectedTagOptions), }); @@ -1105,7 +1115,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Search" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(searchTags), }); @@ -1117,7 +1126,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTagsOptions(enabledTags), }); @@ -1143,11 +1151,8 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(selectedTagOptions), }); - - indexOffset += selectedOptions.length; } if (filteredRecentlyUsedTags.length > 0) { @@ -1157,18 +1162,14 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getTagsOptions(cutRecentlyUsedTags), }); - - indexOffset += filteredRecentlyUsedTags.length; } tagSections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, data: getTagsOptions(filteredTags), }); @@ -1190,8 +1191,8 @@ function hasEnabledTags(policyTagList: Array * @param taxRates - The original tax rates object. * @returns The transformed tax rates object.g */ -function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record { - const defaultTaxKey = taxRates?.defaultExternalID; +function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record { + const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID; const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; @@ -1222,17 +1223,15 @@ function getTaxRatesOptions(taxRates: Array>): Option[] { /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] { const policyRatesSections = []; - const taxes = transformedTaxRates(taxRates); + const taxes = transformedTaxRates(taxRates, defaultTaxKey); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); const numberOfTaxRates = enabledTaxRates.length; - let indexOffset = 0; - // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { const selectedTaxRateOptions = selectedOptions.map((option) => ({ @@ -1244,7 +1243,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "Selected" sectiong title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(selectedTaxRateOptions), }); @@ -1252,13 +1250,12 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO } if (searchInputValue) { - const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName.toLowerCase().includes(searchInputValue.toLowerCase())); + const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); policyRatesSections.push({ // "Search" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(searchTaxRates), }); @@ -1270,7 +1267,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(enabledTaxRates), }); @@ -1278,7 +1274,7 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO } const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const filteredTaxRates = enabledTaxRates.filter((taxRate) => !selectedOptionNames.includes(taxRate.modifiedName)); + const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); if (selectedOptions.length > 0) { const selectedTaxRatesOptions = selectedOptions.map((option) => { @@ -1294,18 +1290,14 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(selectedTaxRatesOptions), }); - - indexOffset += selectedOptions.length; } policyRatesSections.push({ // "All" section when number of items are more than the threshold title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(filteredTaxRates), }); @@ -1384,7 +1376,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1722,7 +1714,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const options = getOptions(reports, personalDetails, { @@ -1757,13 +1749,15 @@ function getShareLogOptions(reports: OnyxCollection, personalDetails: On includePersonalDetails: true, forcePolicyNamePreview: true, includeOwnedWorkspaceChats: true, + includeSelfDM: true, + includeThreads: true, }); } /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails { +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText?: string): PayeePersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), @@ -1776,7 +1770,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person id: personalDetail.accountID, }, ], - descriptiveText: amountText, + descriptiveText: amountText ?? '', login: personalDetail.login ?? '', accountID: personalDetail.accountID, keyForList: String(personalDetail.accountID), @@ -1786,7 +1780,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string): Participant[] { +function getIOUConfirmationOptionsFromParticipants(participants: Array, amountText: string): Array { return participants.map((participant) => ({ ...participant, descriptiveText: amountText, @@ -1809,7 +1803,7 @@ function getFilteredOptions( categories: PolicyCategories = {}, recentlyUsedCategories: string[] = [], includeTags = false, - tags: Record = {}, + tags: PolicyTags | Array = {}, recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, @@ -1981,7 +1975,6 @@ function formatSectionsFromSearchTerm( filteredRecentReports: ReportUtils.OptionData[], filteredPersonalDetails: ReportUtils.OptionData[], maxOptionsSelected: boolean, - indexOffset = 0, personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, ): SectionForSearchTerm { @@ -1999,9 +1992,7 @@ function formatSectionsFromSearchTerm( }) : selectedOptions, shouldShow: selectedOptions.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedOptions.length, }; } @@ -2025,9 +2016,7 @@ function formatSectionsFromSearchTerm( }) : selectedParticipantsWithoutDetails, shouldShow: selectedParticipantsWithoutDetails.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedParticipantsWithoutDetails.length, }; } @@ -2056,12 +2045,15 @@ export { getEnabledCategoriesCount, hasEnabledOptions, sortCategories, + sortTags, getCategoryOptionTree, hasEnabledTags, formatMemberForList, formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + getReportOption, + getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions}; +export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index c3c84cdc2c83..d89c81f58262 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -689,7 +689,6 @@ describe('OptionsListUtils', () => { { name: 'Medical', enabled: true, - isSelected: true, }, ]; const smallCategoriesList = { @@ -714,7 +713,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Food', @@ -747,7 +745,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -772,7 +769,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -838,7 +834,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -846,14 +841,13 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: true, + isSelected: false, }, ], }, { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'Restaurant', @@ -868,7 +862,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, data: [ { text: 'Cars', @@ -965,7 +958,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -998,7 +990,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1007,7 +998,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -1015,7 +1005,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: true, + isSelected: false, }, ], }, @@ -1112,7 +1102,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -1143,7 +1132,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1159,7 +1147,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1213,7 +1200,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Medical', @@ -1227,7 +1213,6 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'HR', @@ -1241,7 +1226,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, // data sorted alphabetically by name data: [ { @@ -1300,7 +1284,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1323,7 +1306,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -2059,6 +2041,230 @@ describe('OptionsListUtils', () => { expect(OptionsListUtils.sortCategories(categoriesIncorrectOrdering3)).toStrictEqual(result3); }); + it('sortTags', () => { + const createTagObjects = (names) => _.map(names, (name) => ({name, enabled: true})); + + const unorderedTagNames = ['10bc', 'b', '0a', '1', '中国', 'b10', '!', '2', '0', '@', 'a1', 'a', '3', 'b1', '日本', '$', '20', '20a', '#', 'a20', 'c', '10']; + const expectedOrderNames = ['!', '#', '$', '0', '0a', '1', '10', '10bc', '2', '20', '20a', '3', '@', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c', '中国', '日本']; + const unorderedTags = createTagObjects(unorderedTagNames); + const expectedOrder = createTagObjects(expectedOrderNames); + expect(OptionsListUtils.sortTags(unorderedTags)).toStrictEqual(expectedOrder); + + const unorderedTagNames2 = ['0', 'a1', '1', 'b1', '3', '10', 'b10', 'a', '2', 'c', '20', 'a20', 'b']; + const expectedOrderNames2 = ['0', '1', '10', '2', '20', '3', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c']; + const unorderedTags2 = createTagObjects(unorderedTagNames2); + const expectedOrder2 = createTagObjects(expectedOrderNames2); + expect(OptionsListUtils.sortTags(unorderedTags2)).toStrictEqual(expectedOrder2); + + const unorderedTagNames3 = [ + '61', + '39', + '97', + '93', + '77', + '71', + '22', + '27', + '30', + '64', + '91', + '24', + '33', + '60', + '21', + '85', + '59', + '76', + '42', + '67', + '13', + '96', + '84', + '44', + '68', + '31', + '62', + '87', + '50', + '4', + '100', + '12', + '28', + '49', + '53', + '5', + '45', + '14', + '55', + '78', + '11', + '35', + '75', + '18', + '9', + '80', + '54', + '2', + '34', + '48', + '81', + '6', + '73', + '15', + '98', + '25', + '8', + '99', + '17', + '90', + '47', + '1', + '10', + '38', + '66', + '57', + '23', + '86', + '29', + '3', + '65', + '74', + '19', + '56', + '63', + '20', + '7', + '32', + '46', + '70', + '26', + '16', + '83', + '37', + '58', + '43', + '36', + '69', + '79', + '72', + '41', + '94', + '95', + '82', + '51', + '52', + '89', + '88', + '40', + '92', + ]; + const expectedOrderNames3 = [ + '1', + '10', + '100', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '2', + '20', + '21', + '22', + '23', + '24', + '25', + '26', + '27', + '28', + '29', + '3', + '30', + '31', + '32', + '33', + '34', + '35', + '36', + '37', + '38', + '39', + '4', + '40', + '41', + '42', + '43', + '44', + '45', + '46', + '47', + '48', + '49', + '5', + '50', + '51', + '52', + '53', + '54', + '55', + '56', + '57', + '58', + '59', + '6', + '60', + '61', + '62', + '63', + '64', + '65', + '66', + '67', + '68', + '69', + '7', + '70', + '71', + '72', + '73', + '74', + '75', + '76', + '77', + '78', + '79', + '8', + '80', + '81', + '82', + '83', + '84', + '85', + '86', + '87', + '88', + '89', + '9', + '90', + '91', + '92', + '93', + '94', + '95', + '96', + '97', + '98', + '99', + ]; + const unorderedTags3 = createTagObjects(unorderedTagNames3); + const expectedOrder3 = createTagObjects(expectedOrderNames3); + expect(OptionsListUtils.sortTags(unorderedTags3)).toStrictEqual(expectedOrder3); + }); + it('getFilteredOptions() for taxRate', () => { const search = 'rate'; const emptySearch = ''; @@ -2089,7 +2295,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2142,7 +2347,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2166,7 +2370,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; From db7f8b1bb2aee7ef410df10a16c0f441c3734a47 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 12:53:25 +0530 Subject: [PATCH 14/28] show selected categories when searching. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7e4082bff481..6c3d45b9b588 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -967,7 +967,7 @@ function getCategoryListSections( ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - + const enabledAndSelectedCategories = [...selectedOptions, ...enabledCategories]; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; @@ -985,7 +985,7 @@ function getCategoryListSections( if (searchInputValue) { const searchCategories: Category[] = []; - enabledCategories.forEach((category) => { + enabledAndSelectedCategories.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } @@ -1088,11 +1088,12 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; - const numberOfTags = enabledTags.length; + const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); + const enabledAndSelectedTags = [...selectedOptions, ...enabledTags]; + const numberEnabledOfTags = enabledTags.length; // If all tags are disabled but there's a previously selected tag, show only the selected tag - if (numberOfTags === 0 && selectedOptions.length > 0) { + if (numberEnabledOfTags === 0 && selectedOptions.length > 0) { const selectedTagOptions = selectedOptions.map((option) => ({ name: option.name, // Should be marked as enabled to be able to be de-selected @@ -1109,7 +1110,7 @@ function getTagListSections( } if (searchInputValue) { - const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const searchTags = enabledAndSelectedTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); tagSections.push({ // "Search" section @@ -1121,7 +1122,7 @@ function getTagListSections( return tagSections; } - if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { + if (numberEnabledOfTags < CONST.TAG_LIST_THRESHOLD) { tagSections.push({ // "All" section when items amount less than the threshold title: '', From 686c587c94116fae91aa5cad642d4a5a7acc4f29 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 13:36:55 +0530 Subject: [PATCH 15/28] update TagPicker to use SelectionList. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 45 ++++++++++++++++++++---------- src/libs/OptionsListUtils.ts | 37 ++++++++++++++---------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index af8acd19e8c4..8eeb0edd22f3 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -3,6 +3,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import OptionsSelector from '@components/OptionsSelector'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -77,6 +79,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe name: selectedTag, enabled: true, accountID: null, + isSelected: true, }, ]; }, [selectedTag]); @@ -100,25 +103,37 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( - + + ); } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6c3d45b9b588..593f5797415d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -99,6 +99,12 @@ type Category = { isSelected?: boolean; }; +type Tag = { + name: string; + enabled: boolean; + isSelected?: boolean; +}; + type Hierarchy = Record; type GetOptionsConfig = { @@ -1061,7 +1067,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Array>): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1071,6 +1077,7 @@ function getTagsOptions(tags: Array>): Optio searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, + isSelected: tag.isSelected, }; }); } @@ -1094,23 +1101,29 @@ function getTagListSections( // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberEnabledOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to be de-selected - enabled: true, - })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedOptions), }); return tagSections; } if (searchInputValue) { - const searchTags = enabledAndSelectedTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const searchTags: Tag[] = []; + + enabledAndSelectedTags.forEach((tag) => { + if (!PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())) { + return; + } + + searchTags.push({ + ...tag, + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === tag.name), + }); + }); tagSections.push({ // "Search" section @@ -1142,17 +1155,11 @@ function getTagListSections( const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to unselect even though the selected category is disabled - enabled: true, - })); - tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedOptions), }); } From 75c53825d392a20e97e464233ab780ea0b6c23a1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 15:52:17 +0530 Subject: [PATCH 16/28] remove redundant code. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 35 +----------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 8eeb0edd22f3..c071895cfdd9 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -1,13 +1,9 @@ import React, {useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {EdgeInsets} from 'react-native-safe-area-context'; -import OptionsSelector from '@components/OptionsSelector'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; @@ -43,12 +39,6 @@ type TagPickerProps = TagPickerOnyxProps & { /** Callback to submit the selected tag */ onSubmit: () => void; - /** - * Safe area insets required for reflecting the portion of the view, - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - */ - insets: EdgeInsets; - /** Should show the selected option that is disabled? */ shouldShowDisabledAndSelectedOption?: boolean; @@ -56,9 +46,7 @@ type TagPickerProps = TagPickerOnyxProps & { tagListIndex: number; }; -function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); +function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) { const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -103,27 +91,6 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( - // - Date: Sun, 31 Mar 2024 18:57:33 +0530 Subject: [PATCH 17/28] added disabled styles without disabling the select/unselect functionality. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 1 - .../SelectionList/RadioListItem.tsx | 2 +- src/components/SelectionList/types.ts | 3 + src/libs/OptionsListUtils.ts | 56 +++++++++++++++---- src/libs/ReportUtils.ts | 1 + 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 3033bf118e8f..c3ac3d8d2f8f 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -71,7 +71,6 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories]); const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((category) => category.searchText === selectedCategory)[0]?.keyForList, [sections, selectedCategory]); - return ( ; @@ -933,6 +935,7 @@ function getCategoryOptionTree(options: Record | Category[], i tooltipText: option.name, isDisabled: !option.enabled, isSelected: !!option.isSelected, + applyDisabledStyle: option.applyDisabledStyle, }); return; @@ -972,8 +975,25 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - const enabledAndSelectedCategories = [...selectedOptions, ...enabledCategories]; + const selectedOptionsWithDisabledStyle: Category[] = []; + const enabledCategoriesName: string[] = []; + const selectedOptionNames: string[] = []; + + const enabledCategories = Object.values(sortedCategories).filter((category) => { + if (category.enabled) { + enabledCategoriesName.push(category.name); + } + return category.enabled; + }); + selectedOptions.forEach((option) => { + selectedOptionNames.push(option.name); + selectedOptionsWithDisabledStyle.push({ + ...option, + applyDisabledStyle: !enabledCategoriesName.includes(option.name), + }); + }); + + const enabledAndSelectedCategories = [...selectedOptionsWithDisabledStyle, ...enabledCategories]; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; @@ -982,7 +1002,7 @@ function getCategoryListSections( // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), }); return categorySections; @@ -1016,11 +1036,10 @@ function getCategoryListSections( // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), }); } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { @@ -1067,7 +1086,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Array>): Option[] { +function getTagsOptions(tags: Tag[]): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1078,6 +1097,7 @@ function getTagsOptions(tags: Array tooltipText: cleanedName, isDisabled: !tag.enabled, isSelected: tag.isSelected, + applyDisabledStyle: tag.applyDisabledStyle, }; }); } @@ -1094,9 +1114,23 @@ function getTagListSections( ) { const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); - const enabledAndSelectedTags = [...selectedOptions, ...enabledTags]; + const selectedOptionNames: string[] = []; + const enabledTagsName: string[] = []; + const selectedOptionsWithDisabledStyle: Category[] = []; + const enabledTags = sortedTags.filter((tag) => { + if (tag.enabled) { + enabledTagsName.push(tag.name); + } + return tag.enabled && !selectedOptionNames.includes(tag.name); + }); + selectedOptions.forEach((option) => { + selectedOptionNames.push(option.name); + selectedOptionsWithDisabledStyle.push({ + ...option, + applyDisabledStyle: !enabledTagsName.includes(option.name), + }); + }); + const enabledAndSelectedTags = [...selectedOptionsWithDisabledStyle, ...enabledTags]; const numberEnabledOfTags = enabledTags.length; // If all tags are disabled but there's a previously selected tag, show only the selected tag @@ -1105,7 +1139,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedOptions), + data: getTagsOptions(selectedOptionsWithDisabledStyle), }); return tagSections; @@ -1159,7 +1193,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedOptions), + data: getTagsOptions(selectedOptionsWithDisabledStyle), }); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9fa28535a7a7..9ce996d52bbd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -407,6 +407,7 @@ type OptionData = { descriptiveText?: string; notificationPreference?: NotificationPreference | null; isDisabled?: boolean | null; + applyDisabledStyle?: boolean | null; name?: string | null; isSelfDM?: boolean | null; } & Report; From 2810b865debb0c3979cbc4522b868c60247d32f3 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 19:18:56 +0530 Subject: [PATCH 18/28] minor changes. Signed-off-by: Krishna Gupta --- src/components/SelectionList/RadioListItem.tsx | 1 + src/libs/OptionsListUtils.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 4e114c236896..a1258bd59424 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -55,6 +55,7 @@ function RadioListItem({ styles.sidebarLinkTextBold, isMultilineSupported ? styles.preWrap : styles.pre, item.alternateText ? styles.mb1 : null, + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ (isDisabled || item.applyDisabledStyle) && styles.colorMuted, isMultilineSupported ? {paddingLeft} : null, ]} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 20e4543d178e..61caf716f209 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -985,6 +985,7 @@ function getCategoryListSections( } return category.enabled; }); + selectedOptions.forEach((option) => { selectedOptionNames.push(option.name); selectedOptionsWithDisabledStyle.push({ @@ -997,7 +998,7 @@ function getCategoryListSections( const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; - if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + if (numberOfEnabledCategories === 0 && selectedOptionsWithDisabledStyle.length > 0) { categorySections.push({ // "Selected" section title: '', @@ -1017,7 +1018,7 @@ function getCategoryListSections( } searchCategories.push({ ...category, - isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), + isSelected: selectedOptionNames.includes(category.name), }); }); @@ -1031,7 +1032,7 @@ function getCategoryListSections( return categorySections; } - if (selectedOptions.length > 0) { + if (selectedOptionsWithDisabledStyle.length > 0) { categorySections.push({ // "Selected" section title: '', @@ -1134,7 +1135,7 @@ function getTagListSections( const numberEnabledOfTags = enabledTags.length; // If all tags are disabled but there's a previously selected tag, show only the selected tag - if (numberEnabledOfTags === 0 && selectedOptions.length > 0) { + if (numberEnabledOfTags === 0 && selectedOptionsWithDisabledStyle.length > 0) { tagSections.push({ // "Selected" section title: '', @@ -1155,7 +1156,7 @@ function getTagListSections( searchTags.push({ ...tag, - isSelected: selectedOptions.some((selectedOption) => selectedOption.name === tag.name), + isSelected: selectedOptionNames.includes(tag.name), }); }); @@ -1188,7 +1189,7 @@ function getTagListSections( .map((tag) => ({name: tag, enabled: true})); const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); - if (selectedOptions.length) { + if (selectedOptionsWithDisabledStyle.length) { tagSections.push({ // "Selected" section title: '', From f7470d6d999a56a28be4ddb38cb9573e8d60bd2e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 31 Mar 2024 19:29:46 +0530 Subject: [PATCH 19/28] fix: tag not shown as selected. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 61caf716f209..4f7e8b502a71 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1175,7 +1175,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags), + data: getTagsOptions(enabledAndSelectedTags), }); return tagSections; From f0dd8d5889b4c9d91c1175527f350b4b5c7a61ce Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 18:39:39 +0530 Subject: [PATCH 20/28] fix: merge conflicts. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 23 +- src/libs/OptionsListUtils.ts | 762 +++++++++++++++++++---------- 2 files changed, 529 insertions(+), 256 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index c071895cfdd9..97cd9aa5c691 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -4,8 +4,10 @@ import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import type * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; @@ -13,7 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; - accountID: number | null; + accountID: number | undefined; }; type TagPickerOnyxProps = { @@ -30,14 +32,14 @@ type TagPickerProps = TagPickerOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string; - /** The selected tag of the money request */ + /** The selected tag of the expense */ selectedTag: string; /** The name of tag list we are getting tags for */ tagListName: string; /** Callback to submit the selected tag */ - onSubmit: () => void; + onSubmit: (selectedTag: Partial) => void; /** Should show the selected option that is disabled? */ shouldShowDisabledAndSelectedOption?: boolean; @@ -47,6 +49,7 @@ type TagPickerProps = TagPickerOnyxProps & { }; function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -66,8 +69,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe { name: selectedTag, enabled: true, - accountID: null, - isSelected: true, + accountID: undefined, }, ]; }, [selectedTag]); @@ -82,7 +84,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, + () => OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); @@ -92,15 +94,16 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe return ( ); } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 4f7e8b502a71..2aad4179c337 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -39,6 +39,7 @@ import times from '@src/utils/times'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; +import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -56,6 +57,15 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type SearchOption = ReportUtils.OptionData & { + item: T; +}; + +type OptionList = { + reports: Array>; + personalDetails: Array>; +}; + type Option = Partial; /** @@ -89,22 +99,31 @@ type CategorySection = CategorySectionBase & { data: Option[]; }; +type TaxRatesOption = { + text?: string; + code?: string; + searchText?: string; + tooltipText?: string; + isDisabled?: boolean; + keyForList?: string; + data: Partial; +}; + +type TaxSection = { + title: string | undefined; + shouldShow: boolean; + data: TaxRatesOption[]; +}; + type CategoryTreeSection = CategorySectionBase & { data: OptionTree[]; + indexOffset?: number; }; type Category = { name: string; enabled: boolean; isSelected?: boolean; - applyDisabledStyle?: boolean; -}; - -type Tag = { - name: string; - enabled: boolean; - isSelected?: boolean; - applyDisabledStyle?: boolean; }; type Hierarchy = Record; @@ -140,6 +159,9 @@ type GetOptionsConfig = { includeSelectedOptions?: boolean; includeTaxRates?: boolean; taxRates?: TaxRatesWithDefault; + includePolicyReportFieldOptions?: boolean; + policyReportFieldOptions?: string[]; + recentlyUsedPolicyReportFieldOptions?: string[]; transactionViolations?: OnyxCollection; }; @@ -149,7 +171,7 @@ type MemberForList = { keyForList: string; isSelected: boolean; isDisabled: boolean; - accountID?: number | null; + accountID?: number; login: string; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; @@ -159,7 +181,7 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; }; -type GetOptions = { +type Options = { recentReports: ReportUtils.OptionData[]; personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; @@ -167,9 +189,10 @@ type GetOptions = { categoryOptions: CategoryTreeSection[]; tagOptions: CategorySection[]; taxRatesOptions: CategorySection[]; + policyReportFieldOptions?: CategorySection[] | null; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -336,7 +359,7 @@ function isPersonalDetailsReady(personalDetails: OnyxEntry) /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData | Participant, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; @@ -517,6 +540,28 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } +/** + * Update alternate text for the option when applicable + */ +function getAlternateText( + option: ReportUtils.OptionData, + {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, +) { + if (!!option.isThread || !!option.isMoneyRequestReport) { + return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { + return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; + } + if (option.isTaskReport) { + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + + return showChatPreviewLine && option.lastMessageText + ? option.lastMessageText + : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); +} + /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -554,7 +599,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage( + const reportPreviewMessage = ReportUtils.getReportPreviewMessage( !isEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReportAction, true, @@ -563,10 +608,11 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails true, lastReportAction, ); + lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(reportPreviewMessage); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { - lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report); + lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report, true); } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); } else if (ReportActionUtils.isPendingRemove(lastReportAction) && ReportActionUtils.isThreadParentMessage(lastReportAction, report?.reportID ?? '')) { @@ -596,26 +642,27 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, + config?: PreviewConfig, ): ReportUtils.OptionData { + const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, - alternateText: null, + alternateText: undefined, pendingAction: undefined, allReportErrors: undefined, brickRoadIndicator: null, icons: undefined, tooltipText: null, ownerAccountID: undefined, - subtitle: null, + subtitle: undefined, participantsList: undefined, accountID: 0, - login: null, + login: undefined, reportID: '', - phoneNumber: null, + phoneNumber: undefined, hasDraftComment: false, - keyForList: null, - searchText: null, + keyForList: undefined, + searchText: undefined, isDefaultRoom: false, isPinned: false, isWaitingOnBankAccount: false, @@ -630,6 +677,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -638,10 +686,8 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; - result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; - if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -659,7 +705,6 @@ function createOption( result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); - result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); @@ -677,22 +722,21 @@ function createOption( let lastMessageText = lastMessageTextFromReport; const lastAction = visibleReportActionItems[report.reportID]; - const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU; + const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU; if (shouldDisplayLastActorName && lastActorDisplayName && lastMessageTextFromReport) { lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (result.isChatRoom || result.isPolicyExpenseChat) { - result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; - } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); - } - reportName = ReportUtils.getReportName(report); + result.lastMessageText = lastMessageText; + + // If displaying chat preview line is needed, let's overwrite the default alternate text + result.alternateText = + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + + reportName = showPersonalDetails + ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') + : ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -835,7 +879,7 @@ function getSearchValueForPhoneOrEmail(searchTerm: string) { * Verifies that there is at least one enabled option */ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { - return Object.values(options).some((option) => option.enabled); + return Object.values(options).some((option) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** @@ -935,7 +979,6 @@ function getCategoryOptionTree(options: Record | Category[], i tooltipText: option.name, isDisabled: !option.enabled, isSelected: !!option.isSelected, - applyDisabledStyle: option.applyDisabledStyle, }); return; @@ -975,35 +1018,19 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const selectedOptionsWithDisabledStyle: Category[] = []; - const enabledCategoriesName: string[] = []; - const selectedOptionNames: string[] = []; - - const enabledCategories = Object.values(sortedCategories).filter((category) => { - if (category.enabled) { - enabledCategoriesName.push(category.name); - } - return category.enabled; - }); + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - selectedOptions.forEach((option) => { - selectedOptionNames.push(option.name); - selectedOptionsWithDisabledStyle.push({ - ...option, - applyDisabledStyle: !enabledCategoriesName.includes(option.name), - }); - }); - - const enabledAndSelectedCategories = [...selectedOptionsWithDisabledStyle, ...enabledCategories]; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; - if (numberOfEnabledCategories === 0 && selectedOptionsWithDisabledStyle.length > 0) { + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), + data, + indexOffset: data.length, }); return categorySections; @@ -1012,43 +1039,50 @@ function getCategoryListSections( if (searchInputValue) { const searchCategories: Category[] = []; - enabledAndSelectedCategories.forEach((category) => { + enabledCategories.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } searchCategories.push({ ...category, - isSelected: selectedOptionNames.includes(category.name), + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), }); }); + const data = getCategoryOptionTree(searchCategories, true); categorySections.push({ // "Search" section title: '', shouldShow: true, - data: getCategoryOptionTree(searchCategories, true), + data, + indexOffset: data.length, }); return categorySections; } - if (selectedOptionsWithDisabledStyle.length > 0) { + if (selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), + data, + indexOffset: data.length, }); } + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1064,19 +1098,23 @@ function getCategoryListSections( if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); + const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); categorySections.push({ // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getCategoryOptionTree(cutRecentlyUsedCategories, true), + data, + indexOffset: data.length, }); } + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1087,7 +1125,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Tag[]): Option[] { +function getTagsOptions(tags: Array>, selectedOptions?: SelectedTagOption[]): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1097,8 +1135,7 @@ function getTagsOptions(tags: Tag[]): Option[] { searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, - isSelected: tag.isSelected, - applyDisabledStyle: tag.applyDisabledStyle, + isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), }; }); } @@ -1115,67 +1152,46 @@ function getTagListSections( ) { const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; - const selectedOptionNames: string[] = []; - const enabledTagsName: string[] = []; - const selectedOptionsWithDisabledStyle: Category[] = []; - const enabledTags = sortedTags.filter((tag) => { - if (tag.enabled) { - enabledTagsName.push(tag.name); - } - return tag.enabled && !selectedOptionNames.includes(tag.name); - }); - selectedOptions.forEach((option) => { - selectedOptionNames.push(option.name); - selectedOptionsWithDisabledStyle.push({ - ...option, - applyDisabledStyle: !enabledTagsName.includes(option.name), - }); - }); - const enabledAndSelectedTags = [...selectedOptionsWithDisabledStyle, ...enabledTags]; - const numberEnabledOfTags = enabledTags.length; + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; + const numberOfTags = enabledTags.length; // If all tags are disabled but there's a previously selected tag, show only the selected tag - if (numberEnabledOfTags === 0 && selectedOptionsWithDisabledStyle.length > 0) { + if (numberOfTags === 0 && selectedOptions.length > 0) { + const selectedTagOptions = selectedOptions.map((option) => ({ + name: option.name, + // Should be marked as enabled to be able to be de-selected + enabled: true, + })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedOptionsWithDisabledStyle), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); return tagSections; } if (searchInputValue) { - const searchTags: Tag[] = []; - - enabledAndSelectedTags.forEach((tag) => { - if (!PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())) { - return; - } - - searchTags.push({ - ...tag, - isSelected: selectedOptionNames.includes(tag.name), - }); - }); + const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); tagSections.push({ // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags), + data: getTagsOptions(searchTags, selectedOptions), }); return tagSections; } - if (numberEnabledOfTags < CONST.TAG_LIST_THRESHOLD) { + if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { tagSections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledAndSelectedTags), + data: getTagsOptions(enabledTags, selectedOptions), }); return tagSections; @@ -1189,12 +1205,18 @@ function getTagListSections( .map((tag) => ({name: tag, enabled: true})); const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); - if (selectedOptionsWithDisabledStyle.length) { + if (selectedOptions.length) { + const selectedTagOptions = selectedOptions.map((option) => ({ + name: option.name, + // Should be marked as enabled to be able to unselect even though the selected category is disabled + enabled: true, + })); + tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedOptionsWithDisabledStyle), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); } @@ -1205,7 +1227,7 @@ function getTagListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getTagsOptions(cutRecentlyUsedTags), + data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); } @@ -1213,7 +1235,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags), + data: getTagsOptions(filteredTags, selectedOptions), }); return tagSections; @@ -1228,15 +1250,91 @@ function hasEnabledTags(policyTagList: Array return hasEnabledOptions(policyTagValueList); } +/** + * Transforms the provided report field options into option objects. + * + * @param reportFieldOptions - an initial report field options array + */ +function getReportFieldOptions(reportFieldOptions: string[]): Option[] { + return reportFieldOptions.map((name) => ({ + text: name, + keyForList: name, + searchText: name, + tooltipText: name, + isDisabled: false, + })); +} + +/** + * Build the section list for report field options + */ +function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: string[], selectedOptions: Array>, searchInputValue: string) { + const reportFieldOptionsSections = []; + const selectedOptionKeys = selectedOptions.map(({text, keyForList, name}) => text ?? keyForList ?? name ?? '').filter((o) => !!o); + let indexOffset = 0; + + if (searchInputValue) { + const searchOptions = options.filter((option) => option.toLowerCase().includes(searchInputValue.toLowerCase())); + + reportFieldOptionsSections.push({ + // "Search" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(searchOptions), + }); + + return reportFieldOptionsSections; + } + + const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((recentlyUsedOption) => !selectedOptionKeys.includes(recentlyUsedOption)); + const filteredOptions = options.filter((option) => !selectedOptionKeys.includes(option)); + + if (selectedOptionKeys.length) { + reportFieldOptionsSections.push({ + // "Selected" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(selectedOptionKeys), + }); + + indexOffset += selectedOptionKeys.length; + } + + if (filteredRecentlyUsedOptions.length > 0) { + reportFieldOptionsSections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredRecentlyUsedOptions), + }); + + indexOffset += filteredRecentlyUsedOptions.length; + } + + reportFieldOptionsSections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredOptions), + }); + + return reportFieldOptionsSections; +} + /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. * * @param taxRates - The original tax rates object. * @returns The transformed tax rates object.g */ -function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record { - const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID; - const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; +function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record { + const defaultTaxKey = taxRates?.defaultExternalID; + const getModifiedName = (data: TaxRate, code: string) => + `${data.name} (${data.value})${defaultTaxKey === code ? ` ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}` : ''}`; const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; } @@ -1252,10 +1350,10 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] { /** * Builds the options for taxRates */ -function getTaxRatesOptions(taxRates: Array>): Option[] { +function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] { return taxRates.map((taxRate) => ({ text: taxRate.modifiedName, - keyForList: taxRate.code, + keyForList: taxRate.modifiedName, searchText: taxRate.modifiedName, tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, @@ -1266,10 +1364,10 @@ function getTaxRatesOptions(taxRates: Array>): Option[] { /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): TaxSection[] { const policyRatesSections = []; - const taxes = transformedTaxRates(taxRates, defaultTaxKey); + const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); @@ -1363,12 +1461,92 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { + const reportMapForAccountIDs: Record = {}; + const allReportOptions: Array> = []; + + if (reports) { + Object.values(reports).forEach((report) => { + if (!report) { + return; + } + + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // Save the report in the map if this is a single participant so we can associate the reportID with the + // personal detail option later. Individuals should not be associated with single participant + // policyExpenseChats or chatRooms since those are not people. + if (accountIDs.length <= 1) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push({ + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }); + }); + } + + const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ + item: personalDetail, + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), + })); + + return { + reports: allReportOptions, + personalDetails: allPersonalDetailsOptions as Array>, + }; +} + +function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { + const accountIDs = report.participantAccountIDs ?? []; + + return { + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }; +} + /** - * Build the options + * Options need to be sorted in the specific order + * @param options - list of options to be sorted + * @param searchValue - search string + * @returns a sorted list of options + */ +function orderOptions(options: ReportUtils.OptionData[], searchValue: string | undefined) { + return lodashOrderBy( + options, + [ + (option) => { + if (!!option.isChatRoom || option.isArchivedRoom) { + return 3; + } + if (!option.login) { + return 2; + } + if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { + return 1; + } + + // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list + return 0; + }, + ], + ['asc'], + ); +} + +/** + * filter options based on specific conditions */ function getOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + options: OptionList, { reportActions = {}, betas = [], @@ -1402,8 +1580,11 @@ function getOptions( includeTaxRates, taxRates, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions = [], + recentlyUsedPolicyReportFieldOptions = [], }: GetOptionsConfig, -): GetOptions { +): Options { if (includeCategories) { const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); @@ -1446,7 +1627,8 @@ function getOptions( }; } - if (!isPersonalDetailsReady(personalDetails)) { + if (includePolicyReportFieldOptions) { + const transformedPolicyReportFieldOptions = getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue); return { recentReports: [], personalDetails: [], @@ -1455,17 +1637,18 @@ function getOptions( categoryOptions: [], tagOptions: [], taxRatesOptions: [], + policyReportFieldOptions: transformedPolicyReportFieldOptions, }; } - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); + const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports ?? {}).filter((report) => { + const filteredReportOptions = options.reports.filter((option) => { + const report = option.item; + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1474,7 +1657,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId() ?? '', + currentReportId: topmostReportId, betas, policies, doesReportHaveViolations, @@ -1487,27 +1670,28 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { + const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { + const report = option.item; + if (option.isArchivedRoom) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReports.reverse(); + orderedReportOptions.reverse(); + + const allReportOptions = orderedReportOptions.filter((option) => { + const report = option.item; - const allReportOptions: ReportUtils.OptionData[] = []; - orderedReports.forEach((report) => { if (!report) { return; } - const isThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); - const isTaskReport = ReportUtils.isTaskReport(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const isSelfDM = ReportUtils.isSelfDM(report); + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; @@ -1536,7 +1720,7 @@ function getOptions( return; } - // In case user needs to add credit bank account, don't allow them to request more money from the workspace. + // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) { return; } @@ -1545,33 +1729,11 @@ function getOptions( return; } - // Save the report in the map if this is a single participant so we can associate the reportID with the - // personal detail option later. Individuals should not be associated with single participant - // policyExpenseChats or chatRooms since those are not people. - if (accountIDs.length <= 1 && !isPolicyExpenseChat && !isChatRoom) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + return option; }); - // We're only picking personal details that have logins set - // This is a temporary fix for all the logic that's been breaking because of the new privacy changes - // See https://github.com/Expensify/Expensify/issues/293465 for more context - // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - const havingLoginPersonalDetails = !includeP2P - ? {} - : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + + const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); + let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1592,8 +1754,17 @@ function getOptions( optionsToExclude.push({login}); }); + let recentReportOptions = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + if (includeRecentReports) { for (const reportOption of allReportOptions) { + /** + * By default, generated options does not have the chat preview line enabled. + * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. + */ + reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); + // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1681,7 +1852,7 @@ function getOptions( !isCurrentUser({login: searchValue} as PersonalDetails) && selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers @@ -1689,7 +1860,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...personalDetails, + ...allPersonalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1721,26 +1892,7 @@ function getOptions( // When sortByReportTypeInSearch is true, recentReports will be returned with all the reports including personalDetailsOptions in the correct Order. recentReportOptions.push(...personalDetailsOptions); personalDetailsOptions = []; - recentReportOptions = lodashOrderBy( - recentReportOptions, - [ - (option) => { - if (!!option.isChatRoom || option.isArchivedRoom) { - return 3; - } - if (!option.login) { - return 2; - } - if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { - return 1; - } - - // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list - return 0; - }, - ], - ['asc'], - ); + recentReportOptions = orderOptions(recentReportOptions, searchValue); } return { @@ -1757,10 +1909,10 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const options = getOptions(reports, personalDetails, { + const optionList = getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1779,11 +1931,11 @@ function getSearchOptions(reports: OnyxCollection, personalDetails: Onyx Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return options; + return optionList; } -function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(reports, personalDetails, { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { + return getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1834,8 +1986,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Array, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1853,29 +2005,38 @@ function getFilteredOptions( includeTaxRates = false, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions: string[] = [], + recentlyUsedPolicyReportFieldOptions: string[] = [], ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - includeRecentReports: true, - includePersonalDetails: true, - maxRecentReportsToShow: 5, - excludeLogins, - includeOwnedWorkspaceChats, - includeP2P, - includeCategories, - categories, - recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, - canInviteUser, - includeSelectedOptions, - includeTaxRates, - taxRates, - includeSelfDM, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + includeRecentReports: true, + includePersonalDetails: true, + maxRecentReportsToShow: 5, + excludeLogins, + includeOwnedWorkspaceChats, + includeP2P, + includeCategories, + categories, + recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, + canInviteUser, + includeSelectedOptions, + includeTaxRates, + taxRates, + includeSelfDM, + includePolicyReportFieldOptions, + policyReportFieldOptions, + recentlyUsedPolicyReportFieldOptions, + }, + ); } /** @@ -1883,8 +2044,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1892,24 +2053,27 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }, + ); } /** @@ -1942,20 +2106,26 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { * Build the options for the Workspace Member Invite view */ function getMemberInviteOptions( - personalDetails: OnyxEntry, + personalDetails: Array>, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, -): GetOptions { - return getOptions({}, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }); + reports: Array> = [], + includeRecentReports = false, +): Options { + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + includeRecentReports, + }, + ); } /** @@ -2063,6 +2233,102 @@ function formatSectionsFromSearchTerm( }; } +/** + * Helper method to get the `keyForList` for the first option in the OptionsList + */ +function getFirstKeyForList(data?: Option[] | null) { + if (!data?.length) { + return ''; + } + + const firstNonEmptyDataObj = data[0]; + + return firstNonEmptyDataObj.keyForList ? firstNonEmptyDataObj.keyForList : ''; +} +/** + * Filters options based on the search input value + */ +function filterOptions(options: Options, searchInputValue: string): Options { + const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); + const searchTerms = searchValue ? searchValue.split(' ') : []; + + // The regex below is used to remove dots only from the local part of the user email (local-part@domain) + // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) + const emailRegex = /\.(?=[^\s@]*@)/g; + + const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { + const keys: string[] = []; + const visibleChatMemberAccountIDs = item.participantsList ?? []; + if (allPersonalDetails) { + visibleChatMemberAccountIDs.forEach((participant) => { + const login = participant?.login; + + if (participant?.displayName) { + keys.push(participant.displayName); + } + + if (login) { + keys.push(login); + keys.push(login.replace(emailRegex, '')); + } + }); + } + + return keys; + }; + const matchResults = searchTerms.reduceRight((items, term) => { + const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { + let values: string[] = []; + if (item.text) { + values.push(item.text); + } + + if (item.login) { + values.push(item.login); + values.push(item.login.replace(emailRegex, '')); + } + + if (item.isThread) { + if (item.alternateText) { + values.push(item.alternateText); + } + } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { + if (item.subtitle) { + values.push(item.subtitle); + } + } + values = values.concat(getParticipantsLoginsArray(item)); + + return uniqFast(values); + }); + const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => + uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']), + ); + + return { + recentReports: recentReports ?? [], + personalDetails: personalDetails ?? [], + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; + }, options); + + const recentReports = matchResults.recentReports.concat(matchResults.personalDetails); + + return { + personalDetails: [], + recentReports: orderOptions(recentReports, searchValue), + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2095,8 +2361,12 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + filterOptions, + createOptionList, + createOptionFromReport, getReportOption, getTaxRatesSection, + getFirstKeyForList, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; From d7e3e5ce8c420d3ba5d16ed1e2c8cf0a633b1879 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 18:45:47 +0530 Subject: [PATCH 21/28] remove redundant checks. Signed-off-by: Krishna Gupta --- src/components/SelectionList/RadioListItem.tsx | 2 +- src/components/SelectionList/types.ts | 3 --- src/libs/ReportUtils.ts | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 2690b1b47dd4..b38da6639970 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -51,7 +51,7 @@ function RadioListItem({ isMultilineSupported ? styles.preWrap : styles.pre, item.alternateText ? styles.mb1 : null, /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ - (isDisabled || item.applyDisabledStyle) && styles.colorMuted, + isDisabled && styles.colorMuted, isMultilineSupported ? {paddingLeft} : null, ]} numberOfLines={isMultilineSupported ? 2 : 1} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index b06b8c1d528e..a96d6c3abb17 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -77,9 +77,6 @@ type ListItem = { /** Whether this option is disabled for selection */ isDisabled?: boolean | null; - /** To apply diabled style when item is not diabled to unselect */ - applyDisabledStyle?: boolean | null; - /** List title is bold by default. Use this props to customize it */ isBold?: boolean; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 612cad7a659e..af55b4ca29be 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -431,7 +431,6 @@ type OptionData = { descriptiveText?: string; notificationPreference?: NotificationPreference | null; isDisabled?: boolean | null; - applyDisabledStyle?: boolean | null; name?: string | null; isSelfDM?: boolean; reportID?: string; From 8551719144d331b71bbd52389a8a9845198f8d62 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 21:33:35 +0530 Subject: [PATCH 22/28] make list item deselectable if disabled but has been selected. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 1 - src/components/SelectionList/BaseListItem.tsx | 2 +- src/libs/OptionsListUtils.ts | 15 ++++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index f1cab01bcc0f..d3dbdf28f2de 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -37,7 +37,6 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return [ { name: selectedCategory, - enabled: true, accountID: undefined, isSelected: true, }, diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 9e6fb31d0316..3b8e4e633d68 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -66,7 +66,7 @@ function BaseListItem({ {...bind} ref={pressableRef} onPress={() => onSelectRow(item)} - disabled={isDisabled} + disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2aad4179c337..4c60e049590c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1019,12 +1019,21 @@ function getCategoryListSections( ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - + const enabledCategoriesNames = enabledCategories.map((category) => category.name); + const selectedOptionsWithDisabledState: Category[] = []; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; + selectedOptions.forEach((option) => { + if (enabledCategoriesNames.includes(option.name)) { + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: true}); + return; + } + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); + }); + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', @@ -1062,7 +1071,7 @@ function getCategoryListSections( } if (selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', From d126d63a1bc71860e5fed59910ef2c6bf9884f88 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 21 Apr 2024 00:08:34 +0530 Subject: [PATCH 23/28] make list item deselectable if disabled but has been selected for tags & taxes. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 1 + src/components/TaxPicker.tsx | 7 ++- src/libs/OptionsListUtils.ts | 70 +++++++++++++++--------------- src/types/onyx/Policy.ts | 3 ++ 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 97cd9aa5c691..cbd9418a83e9 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -15,6 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; + isSelected?: boolean; accountID: number | undefined; }; diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 0aed28681d5c..4871979a48cb 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -53,19 +53,18 @@ function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerPr return [ { - name: selectedTaxRate, - enabled: true, + modifiedName: selectedTaxRate, + isDisabled: false, accountID: null, }, ]; }, [selectedTaxRate]); - const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]); + const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Tax[], searchValue), [taxRates, searchValue, selectedOptions]); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue); const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); - return ( ; type GetOptionsConfig = { @@ -1162,21 +1168,26 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; + const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); + const enabledTagsNames = enabledTags.map((tag) => tag.name); + const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; + selectedOptions.forEach((tag) => { + if (enabledTagsNames.includes(tag.name)) { + selectedTagsWithDisabledState.push({...tag, enabled: true}); + return; + } + selectedTagsWithDisabledState.push({...tag, enabled: false}); + }); + // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to be de-selected - enabled: true, - })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); return tagSections; @@ -1215,17 +1226,11 @@ function getTagListSections( const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to unselect even though the selected category is disabled - enabled: true, - })); - tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); } @@ -1367,33 +1372,39 @@ function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, data: taxRate, + isSelected: taxRate.isSelected, })); } /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): TaxSection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Tax[], searchInputValue: string): TaxSection[] { const policyRatesSections = []; const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); + const enabledTaxRatesNames = enabledTaxRates.map((tax) => tax.modifiedName); + const selectedTaxRateWithDisabledState: Tax[] = []; const numberOfTaxRates = enabledTaxRates.length; + selectedOptions.forEach((tax) => { + if (enabledTaxRatesNames.includes(tax.modifiedName)) { + selectedTaxRateWithDisabledState.push({...tax, isDisabled: false, isSelected: true}); + return; + } + selectedTaxRateWithDisabledState.push({...tax, isDisabled: true, isSelected: true}); + }); + // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { - const selectedTaxRateOptions = selectedOptions.map((option) => ({ - modifiedName: option.name, - // Should be marked as enabled to be able to be de-selected - isDisabled: false, - })); policyRatesSections.push({ // "Selected" sectiong title: '', shouldShow: false, - data: getTaxRatesOptions(selectedTaxRateOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); return policyRatesSections; @@ -1423,24 +1434,15 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO return policyRatesSections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); if (selectedOptions.length > 0) { - const selectedTaxRatesOptions = selectedOptions.map((option) => { - const taxRateObject = Object.values(taxes).find((taxRate) => taxRate.modifiedName === option.name); - - return { - modifiedName: option.name, - isDisabled: !!taxRateObject?.isDisabled, - }; - }); - policyRatesSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTaxRatesOptions(selectedTaxRatesOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); } @@ -1623,7 +1625,7 @@ function getOptions( } if (includeTaxRates) { - const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Category[], searchInputValue); + const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Tax[], searchInputValue); return { recentReports: [], @@ -2378,4 +2380,4 @@ export { getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c55d6359c68f..4a4526daafad 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -51,6 +51,9 @@ type TaxRate = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Indicates if the tax rate is disabled. */ isDisabled?: boolean; + /** Indicates if the tax rate is selected. */ + isSelected?: boolean; + /** An error message to display to the user */ errors?: OnyxCommon.Errors; From 4ebf5941363f59dd8acc6bbd9d16bb18729af404 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 21 Apr 2024 00:20:47 +0530 Subject: [PATCH 24/28] fix: seleceted category & tax not showing in search results. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 1 + src/components/TaxPicker.tsx | 1 + src/libs/OptionsListUtils.ts | 11 +++++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index d3dbdf28f2de..3e62af29d16d 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -70,6 +70,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories]); const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((category) => category.searchText === selectedCategory)[0]?.keyForList, [sections, selectedCategory]); + return ( 0, searchValue); const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); + return ( { + categoriesForSearch.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } @@ -1168,7 +1169,7 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); + const enabledTags = sortedTags.filter((tag) => tag.enabled); const enabledTagsNames = enabledTags.map((tag) => tag.name); const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; @@ -1194,13 +1195,15 @@ function getTagListSections( } if (searchInputValue) { - const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const enabledSearchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; tagSections.push({ // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags, selectedOptions), + data: getTagsOptions(tagsForSearch, selectedOptions), }); return tagSections; From 8fd696e63cb28bd3b9d64ead3e03658b2c755879 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 24 Apr 2024 17:28:05 +0530 Subject: [PATCH 25/28] fix: dubplicate taxes shown & selected disabled tax not showing when searched. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f951402887ac..713eb9df716e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1388,8 +1388,10 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); const enabledTaxRatesNames = enabledTaxRates.map((tax) => tax.modifiedName); + const enabledTaxRatesWithoutSelectedOptions = enabledTaxRates.filter((tax) => tax.modifiedName && !selectedOptionNames.includes(tax.modifiedName)); const selectedTaxRateWithDisabledState: Tax[] = []; const numberOfTaxRates = enabledTaxRates.length; @@ -1414,13 +1416,15 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO } if (searchInputValue) { - const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const enabledSearchTaxRates = enabledTaxRatesWithoutSelectedOptions.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTaxRateWithDisabledState.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const taxesForSearch = [...selectedSearchTags, ...enabledSearchTaxRates]; policyRatesSections.push({ // "Search" section title: '', shouldShow: true, - data: getTaxRatesOptions(searchTaxRates), + data: getTaxRatesOptions(taxesForSearch), }); return policyRatesSections; @@ -1431,15 +1435,12 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTaxRatesOptions(enabledTaxRates), + data: getTaxRatesOptions([...selectedTaxRateWithDisabledState, ...enabledTaxRatesWithoutSelectedOptions]), }); return policyRatesSections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); - const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); - if (selectedOptions.length > 0) { policyRatesSections.push({ // "Selected" section @@ -1453,7 +1454,7 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when number of items are more than the threshold title: '', shouldShow: true, - data: getTaxRatesOptions(filteredTaxRates), + data: getTaxRatesOptions(enabledTaxRatesWithoutSelectedOptions), }); return policyRatesSections; From c03ded65d326535a5d1f2e62c26761e4f4fbf0d9 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 24 Apr 2024 17:46:13 +0530 Subject: [PATCH 26/28] fix: Tag - duplicate selected options shown and seleceted disbaled option not shown when searched Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 713eb9df716e..33854931349b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1171,6 +1171,7 @@ function getTagListSections( const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const enabledTags = sortedTags.filter((tag) => tag.enabled); const enabledTagsNames = enabledTags.map((tag) => tag.name); + const enabledTagsWithoutSelectedOptions = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; @@ -1195,7 +1196,7 @@ function getTagListSections( } if (searchInputValue) { - const enabledSearchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; @@ -1214,7 +1215,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags, selectedOptions), + data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), }); return tagSections; @@ -1226,7 +1227,6 @@ function getTagListSections( return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag); }) .map((tag) => ({name: tag, enabled: true})); - const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { tagSections.push({ @@ -1252,7 +1252,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags, selectedOptions), + data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), }); return tagSections; From d58f5fb91908c4ce3272c8adc64374c1145b004e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 29 Apr 2024 22:11:29 +0530 Subject: [PATCH 27/28] remove redundant eslint comment. Signed-off-by: Krishna Gupta --- src/components/SelectionList/RadioListItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 0c57023c1d24..b595008e4e3b 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -52,7 +52,6 @@ function RadioListItem({ styles.sidebarLinkTextBold, isMultilineSupported ? styles.preWrap : styles.pre, item.alternateText ? styles.mb1 : null, - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ isDisabled && styles.colorMuted, isMultilineSupported ? {paddingLeft} : null, ]} From a7f8a29f3c1398edad4231cdf89ae24f9e8de8db Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 1 May 2024 10:57:54 +0530 Subject: [PATCH 28/28] fix: Jest Unit Tests. Signed-off-by: Krishna Gupta --- tests/unit/OptionsListUtilsTest.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 76b4324f697b..0df4e2fe124b 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1067,8 +1067,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -1236,8 +1236,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -2587,6 +2587,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax exempt 1 (0%) • Default', tooltipText: 'Tax exempt 1 (0%) • Default', isDisabled: undefined, + isSelected: undefined, // creates a data option. data: { name: 'Tax exempt 1', @@ -2601,6 +2602,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax option 3 (5%)', tooltipText: 'Tax option 3 (5%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax option 3', code: 'CODE3', @@ -2614,6 +2616,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2', @@ -2637,6 +2640,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2',