From 0f63c8a1867407a9cc474866f6f18f06dd4328b1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 28 Feb 2024 14:55:53 +0530 Subject: [PATCH 01/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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 9321b99225a23576a0501e8106647d941ad06009 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 4 Apr 2024 00:33:07 +0700 Subject: [PATCH 20/81] add solution --- src/libs/actions/Report.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a27f92ef8f57..852624381501 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -90,6 +90,7 @@ import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; +import getDraftComment from '@libs/ComposerUtils/getDraftComment'; type SubscriberCallback = (isFromCurrentUser: boolean, reportActionID: string | undefined) => void; @@ -1116,6 +1117,8 @@ function handleReportChanged(report: OnyxEntry) { // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. if (report?.reportID && report.preexistingReportID) { + const draftComment = getDraftComment(report.reportID); + saveReportComment(report.preexistingReportID, draftComment ?? ''); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); // Only re-route them if they are still looking at the optimistically created report From f0dd8d5889b4c9d91c1175527f350b4b5c7a61ce Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 18:39:39 +0530 Subject: [PATCH 21/81] 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 22/81] 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 23/81] 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 24/81] 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 25/81] 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 26/81] 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 27/81] 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 f04eec8e95cda3ac78392fa7d078465965766f7b Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 12:47:49 +0700 Subject: [PATCH 28/81] feat create automated tests for Workspace Taxes page --- tests/actions/PolicyTaxTest.ts | 512 +++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 tests/actions/PolicyTaxTest.ts diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts new file mode 100644 index 000000000000..96ce6274ee3c --- /dev/null +++ b/tests/actions/PolicyTaxTest.ts @@ -0,0 +1,512 @@ +import Onyx from 'react-native-onyx'; +import {createPolicyTax, deletePolicyTaxes, renamePolicyTax, setPolicyTaxesEnabled, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy as PolicyType, TaxRate} from '@src/types/onyx'; +import createRandomPolicy from '../utils/collections/policies'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyTax', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('SetPolicyCustomTaxName', () => { + it('Set policy`s custom tax name', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const customTaxName = 'Custom tag name'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyCurrencyDefaultTax', () => { + it('Set policy`s currency default tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyForeignCurrencyDefaultTax', () => { + it('Set policy`s foreign currency default', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('CreatePolicyTax', () => { + it('Create a new tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + createPolicyTax(fakePolicy.id, newTaxRate); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeFalsy(); + expect(createdTax?.pendingFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyTaxesEnabled', () => { + it('Disable policy`s taxes', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + + describe('RenamePolicyTax', () => { + it('Rename tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.name).toBeFalsy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('UpdatePolicyTaxValue', () => { + it('Update tax`s value', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.value).toBeFalsy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('DeletePolicyTaxes', () => { + it('Delete tax that is not foreignTaxDefault', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Delete tax that is foreignTaxDefault', () => { + const fakePolicy: PolicyType = { + ...createRandomPolicy(0), + taxRates: { + ...CONST.DEFAULT_TAX, + foreignTaxDefault: 'id_TAX_RATE_1', + }, + }; + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + const firstTaxID = 'id_TAX_EXEMPT'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(taxRates?.foreignTaxDefault).toBe(firstTaxID); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); +}); From 474f687a3f1634e0f4addccf897e9d1064ab54a5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 14:41:48 +0700 Subject: [PATCH 29/81] fix lint --- tests/actions/PolicyTaxTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 96ce6274ee3c..3899e0c2a24e 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -455,7 +455,6 @@ describe('actions/PolicyTax', () => { foreignTaxDefault: 'id_TAX_RATE_1', }, }; - const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; const firstTaxID = 'id_TAX_EXEMPT'; From d58f5fb91908c4ce3272c8adc64374c1145b004e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 29 Apr 2024 22:11:29 +0530 Subject: [PATCH 30/81] 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 31/81] 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', From ec0c3dfb63163718cdd95290aec9a20776f228de Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 2 May 2024 15:30:03 +0700 Subject: [PATCH 32/81] Add final solution --- src/libs/actions/Report.ts | 20 ++++++++++++------- .../ComposerWithSuggestions.tsx | 16 ++++++++++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 58ea252c3c61..60e6682c5fdd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -104,7 +104,6 @@ import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; -import getDraftComment from '@libs/ComposerUtils/getDraftComment'; type SubscriberCallback = (isFromCurrentUser: boolean, reportActionID: string | undefined) => void; @@ -1175,6 +1174,10 @@ function saveReportDraftComment(reportID: string, comment: string | null) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)); } +function saveReportDraftCommentWithCallback(reportID: string, comment: string | null, callback: () => void) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)).then(callback); +} + /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ function broadcastUserIsTyping(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); @@ -1203,15 +1206,17 @@ function handleReportChanged(report: OnyxEntry) { // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. if (report?.reportID && report.preexistingReportID) { - const draftComment = getDraftComment(report.reportID); - saveReportComment(report.preexistingReportID, draftComment ?? ''); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); - + let callback = () => {}; // Only re-route them if they are still looking at the optimistically created report if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) { - // Pass 'FORCED_UP' type to replace new report on second login with proper one in the Navigation - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + callback = () => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); + }; } + DeviceEventEmitter.emit(`switchToCurrentReport_${report.reportID}`, { + preexistingReportID: report.preexistingReportID, + callback, + }); return; } @@ -3712,4 +3717,5 @@ export { leaveGroupChat, removeFromGroupChat, updateGroupChatMemberRoles, + saveReportDraftCommentWithCallback, }; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 469a7300a84f..fe827ecaa69c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputSelectionChangeEventData, } from 'react-native'; -import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; +import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {useAnimatedRef} from 'react-native-reanimated'; @@ -344,6 +344,20 @@ function ComposerWithSuggestions( [], ); + useEffect(() => { + const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToCurrentReport_${reportID}`, ({preexistingReportID, callback}) => { + if (!commentRef.current) { + callback(); + return; + } + Report.saveReportDraftCommentWithCallback(preexistingReportID, commentRef.current, callback); + }); + + return () => { + switchToCurrentReport.remove(); + }; + }, [reportID]); + /** * Find the newly added characters between the previous text and the new text based on the selection. * From fc2de8c88b2d91a538c2fe8c0c4807ba8aa06ca2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 16:52:35 +0700 Subject: [PATCH 33/81] fix add fail test case --- tests/actions/PolicyTaxTest.ts | 482 +++++++++++++++++++++++++++++---- 1 file changed, 432 insertions(+), 50 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 3899e0c2a24e..c35aea14e4f8 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -11,6 +11,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; OnyxUpdateManager(); describe('actions/PolicyTax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -20,22 +21,21 @@ describe('actions/PolicyTax', () => { beforeEach(() => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); - return Onyx.clear().then(waitForBatchedUpdates); + return Onyx.clear() + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy)) + .then(waitForBatchedUpdates); }); describe('SetPolicyCustomTaxName', () => { it('Set policy`s custom tax name', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const customTaxName = 'Custom tag name'; + const originalCustomTaxName = fakePolicy?.taxRates?.name; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -72,20 +72,65 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s custom tax name when API returns an error', () => { + const customTaxName = 'Custom tag name'; + const originalCustomTaxName = fakePolicy?.taxRates?.name; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(originalCustomTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.name).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); + describe('SetPolicyCurrencyDefaultTax', () => { it('Set policy`s currency default tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxCode = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -122,20 +167,64 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s currency default tax when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultExternalID = fakePolicy?.taxRates?.defaultExternalID; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(originalDefaultExternalID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.defaultExternalID).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('SetPolicyForeignCurrencyDefaultTax', () => { it('Set policy`s foreign currency default', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxCode = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -173,10 +262,59 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s foreign currency default when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultForeignCurrencyID = fakePolicy?.taxRates?.foreignTaxDefault; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.foreignTaxDefault).toBe(originalDefaultForeignCurrencyID); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.foreignTaxDefault).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('CreatePolicyTax', () => { it('Create a new tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const newTaxRate: TaxRate = { name: 'Tax rate 2', value: '2%', @@ -185,12 +323,9 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - createPolicyTax(fakePolicy.id, newTaxRate); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -230,19 +365,68 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Remove the optimistic tax if the API returns an error', () => { + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('SetPolicyTaxesEnabled', () => { it('Disable policy`s taxes', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const disableTaxID = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -282,21 +466,68 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Disable policy`s taxes but API returns an error, then enable policy`s taxes again', () => { + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + const originalTaxes = {...fakePolicy?.taxRates?.taxes}; + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBe(!!originalTaxes[disableTaxID].isDisabled); + expect(disabledTax?.errorFields?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('RenamePolicyTax', () => { it('Rename tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxID = 'id_TAX_RATE_1'; const newTaxName = 'Tax rate 1 updated'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - renamePolicyTax(fakePolicy.id, taxID, newTaxName); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -336,21 +567,69 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Rename tax but API returns an error, then recover the original tax`s name', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(originalTaxRate.name); + expect(updatedTax?.errorFields?.name).toBeTruthy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('UpdatePolicyTaxValue', () => { it('Update tax`s value', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxID = 'id_TAX_RATE_1'; const newTaxValue = 10; const stringTaxValue = `${newTaxValue}%`; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -390,21 +669,70 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Update tax`s value but API returns an error, then recover the original tax`s value', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(originalTaxRate.value); + expect(updatedTax?.errorFields?.value).toBeTruthy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('DeletePolicyTaxes', () => { it('Delete tax that is not foreignTaxDefault', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - deletePolicyTaxes(fakePolicy.id, [taxID]); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -461,7 +789,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, {taxRates: {foreignTaxDefault: 'id_TAX_RATE_1'}}) .then(() => { deletePolicyTaxes(fakePolicy.id, [taxID]); return waitForBatchedUpdates(); @@ -507,5 +835,59 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Delete tax that is not foreignTaxDefault but API return an error, then recover the delated tax', () => { + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax?.pendingAction).toBeFalsy(); + expect(deletedTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); }); From c9ad9aa80a601f59090ea5bf767dfad678c9bd6e Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 17:02:43 +0700 Subject: [PATCH 34/81] fix lint --- tests/actions/PolicyTaxTest.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index c35aea14e4f8..3341fe714639 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -29,8 +29,6 @@ describe('actions/PolicyTax', () => { describe('SetPolicyCustomTaxName', () => { it('Set policy`s custom tax name', () => { const customTaxName = 'Custom tag name'; - const originalCustomTaxName = fakePolicy?.taxRates?.name; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); @@ -776,13 +774,6 @@ describe('actions/PolicyTax', () => { }); it('Delete tax that is foreignTaxDefault', () => { - const fakePolicy: PolicyType = { - ...createRandomPolicy(0), - taxRates: { - ...CONST.DEFAULT_TAX, - foreignTaxDefault: 'id_TAX_RATE_1', - }, - }; const taxID = 'id_TAX_RATE_1'; const firstTaxID = 'id_TAX_EXEMPT'; From e37e0b7827cf3ec606c7c4b6a21de34ee18fba4d Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 17:06:38 +0700 Subject: [PATCH 35/81] fix lint --- tests/actions/PolicyTaxTest.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 3341fe714639..a17179d8f7af 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -98,7 +98,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -193,7 +193,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -289,7 +289,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -397,7 +397,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -494,7 +494,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -596,7 +596,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -699,7 +699,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -858,7 +858,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( From c26c9fef183f20e53f3f1ae780aa64793be0b750 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 10:12:47 +0530 Subject: [PATCH 36/81] add export route --- src/ROUTES.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 79f9c84b5998..11752e2c2771 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -786,6 +786,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/taxes` as const, }, + POLICY_ACCOUNTING_XERO_EXPORT: { + route: 'settings/workspaces/:policyID/accounting/xero/export', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const, From b6c49bf8cf268cffaf1083575ae5e0e73eb71749 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 10:13:03 +0530 Subject: [PATCH 37/81] add export screen --- src/SCREENS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ec120657a3bb..249124cf067e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -239,6 +239,7 @@ const SCREENS = { XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', + XERO_EXPORT: 'Policy_Accounting_Xero_Export', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', From a94c5a05dd3c06895a10fe6b777e4facda5fad2b Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 10:13:19 +0530 Subject: [PATCH 38/81] add export to navigation --- .../Navigation/AppNavigator/ModalStackNavigators/index.tsx | 1 + .../Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 3 +++ 4 files changed, 6 insertions(+) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index eb45b5029619..fc6cfc673e03 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -299,6 +299,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: () => require('../../../../pages/workspace/accounting/xero/export/XeroExportConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index d4501bd09044..d982bc3d97ed 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -44,6 +44,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION, SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER, SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES, + SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT, ], [SCREENS.WORKSPACE.TAXES]: [ SCREENS.WORKSPACE.TAXES_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7f9e7eba5003..92c5856d2b5f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -331,6 +331,7 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TAXES.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.route}, [SCREENS.WORKSPACE.DESCRIPTION]: { path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 522a8514587a..37d662683b53 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -324,6 +324,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; From 284a5763b8459f20cbe3fbd66e81a59c78092982 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 10:13:51 +0530 Subject: [PATCH 39/81] navigate to export page --- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 6ebac5c3e3a4..042829610dcb 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -92,7 +92,7 @@ function accountingIntegrationData( /> ), onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.getRoute(policyID)), - onExportPagePress: () => {}, + onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)), onAdvancedPagePress: () => {}, }; default: From d4305698b27d3a6361ef19d78a5da22bff9c1ee1 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 10:14:04 +0530 Subject: [PATCH 40/81] add default export page --- .../export/XeroExportConfigurationPage.tsx | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx new file mode 100644 index 000000000000..f04b58a53da4 --- /dev/null +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {MenuItemProps} from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuItem = MenuItemProps & {pendingAction?: OfflineWithFeedbackProps['pendingAction']}; + +function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; + const policyOwner = policy?.owner ?? ''; + const { + export: exportConfiguration, + exportDate, + reimbursableExpensesExportDestination, + receivableAccount, + nonReimbursableExpensesExportDestination, + errorFields, + pendingFields, + } = policy?.connections?.quickbooksOnline?.config ?? {}; + const menuItems: MenuItem[] = [ + { + description: translate('workspace.qbo.preferredExporter'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER.getRoute(policyID)), + brickRoadIndicator: errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportConfiguration?.exporter ?? policyOwner, + pendingAction: pendingFields?.export, + error: errorFields?.exporter ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.qbo.date'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT.getRoute(policyID)), + brickRoadIndicator: errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportDate ? translate(`workspace.qbo.exportDate.values.${exportDate}.label`) : undefined, + pendingAction: pendingFields?.exportDate, + error: errorFields?.exportDate ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.qbo.exportExpenses'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES.getRoute(policyID)), + brickRoadIndicator: Boolean(errorFields?.exportEntity) || Boolean(errorFields?.reimbursableExpensesAccount) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: reimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${reimbursableExpensesExportDestination}`) : undefined, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + pendingAction: pendingFields?.reimbursableExpensesExportDestination || pendingFields?.reimbursableExpensesAccount, + }, + { + description: translate('workspace.qbo.exportInvoices'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT.getRoute(policyID)), + brickRoadIndicator: errorFields?.receivableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: receivableAccount?.name, + pendingAction: pendingFields?.receivableAccount, + error: errorFields?.receivableAccount ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.qbo.exportCompany'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT.getRoute(policyID)), + brickRoadIndicator: errorFields?.exportCompanyCard ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: nonReimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${nonReimbursableExpensesExportDestination}`) : undefined, + pendingAction: pendingFields?.nonReimbursableExpensesExportDestination, + error: errorFields?.nonReimbursableExpensesExportDestination ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.qbo.exportExpensifyCard'), + title: translate('workspace.qbo.accounts.credit_card'), + shouldShowRightIcon: false, + interactive: false, + }, + ]; + + return ( + + + + + {translate('workspace.qbo.exportDescription')} + {menuItems.map((menuItem) => ( + + + + ))} + + {`${translate('workspace.qbo.deepDiveExpensifyCard')} `} + Link.openExternalLink(CONST.DEEP_DIVE_EXPENSIFY_CARD)} + style={[styles.mutedNormalTextLabel, styles.link]} + > + {translate('workspace.qbo.deepDiveExpensifyCardIntegration')} + + + + + + ); +} + +XeroExportConfigurationPage.displayName = 'XeroExportConfigurationPage'; + +export default withPolicyConnections(XeroExportConfigurationPage); From 460effc410a65f565112269c2f8767c534bf6dac Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 11:50:46 +0530 Subject: [PATCH 41/81] add helper text to menu item --- src/components/MenuItem.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index bf35d65340fc..1f0093c3c72b 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -141,6 +141,11 @@ type MenuItemBaseProps = { /** A description text to show under the title */ description?: string; + /** Text to show below menu item. This text is not interactive */ + helperText?: string; + + helperTextStyle?: StyleProp; + /** Should the description be shown above the title (instead of the other way around) */ shouldShowDescriptionOnTop?: boolean; @@ -296,6 +301,8 @@ function MenuItem( furtherDetailsIcon, furtherDetails, description, + helperText, + helperTextStyle, error, errorText, success = false, @@ -679,6 +686,7 @@ function MenuItem( )} + {!!helperText && {helperText}} ); } From 7b85464c266f7a20e82943fef0341cec347f61fe Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 11:50:56 +0530 Subject: [PATCH 42/81] add lang --- src/languages/en.ts | 16 ++++++++++++++++ src/languages/es.ts | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 625a6fa090f1..488507b6dbd2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2000,6 +2000,22 @@ export default { customersDescription: 'Import customer contacts. Billable expenses need tags for export. Expenses will carry the customer information to Xero for sales invoices.', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', notImported: 'Not imported', + export: 'Export', + exportDescription: 'Configure how data in Expensify gets exported to Xero.', + exportCompanyCard: 'Export company card expenses as', + purchaseBill: 'Purchase Bill', + exportDeepDiveCompanyCard: + 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', + bankTransactions: 'Bank transactions', + xeroBankAccount: 'Xero Bank Account', + preferredExporter: 'Preferred exporter', + exportExpenses: 'Export out-of-pocket expenses as', + exportExpensesDescription: + 'Reports will export as a Purchase Bill awaiting payment, posting on the last day of the month in which expenses were incurred. This is the only export option with Xero.', + purchaseBillDate: 'Purchase Bill Date', + exportInvoices: 'Export invoices as', + salesInvoice: 'Sales invoice', + exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', }, type: { free: 'Free', diff --git a/src/languages/es.ts b/src/languages/es.ts index b82ee4dd69ea..3bb74b80caa2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2033,6 +2033,22 @@ export default { 'Importar contactos de clientes. Los gastos facturables necesitan etiquetas para la exportación. Los gastos llevarán la información del cliente a Xero para las facturas de ventas.', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', notImported: 'No importado', + export: 'Exportar', + exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', + exportCompanyCard: 'Export company card expenses as', + purchaseBill: 'Purchase Bill', + exportDeepDiveCompanyCard: + 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', + bankTransactions: 'Bank transactions', + xeroBankAccount: 'Xero días laborales', + preferredExporter: 'Exportador preferido', + exportExpenses: 'Exportar gastos de bolsillo como', + exportExpensesDescription: + 'Reports will export as a Purchase Bill awaiting payment, posting on the last day of the month in which expenses were incurred. This is the only export option with Xero.', + purchaseBillDate: 'Purchase Bill Date', + exportInvoices: 'Exportar facturas como', + salesInvoice: 'Sales invoice', + exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', }, type: { free: 'Gratis', From 70c405d1b5514b618d94f101c15dade17f6ceef8 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 11:51:19 +0530 Subject: [PATCH 43/81] add default export page --- .../export/XeroExportConfigurationPage.tsx | 89 +++++++------------ 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx index f04b58a53da4..688eb693906f 100644 --- a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -7,16 +7,12 @@ import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; type MenuItem = MenuItemProps & {pendingAction?: OfflineWithFeedbackProps['pendingAction']}; @@ -25,61 +21,52 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const policyOwner = policy?.owner ?? ''; - const { - export: exportConfiguration, - exportDate, - reimbursableExpensesExportDestination, - receivableAccount, - nonReimbursableExpensesExportDestination, - errorFields, - pendingFields, - } = policy?.connections?.quickbooksOnline?.config ?? {}; + const {export: exportConfiguration, errorFields, pendingFields} = policy?.connections?.xero?.config ?? {}; const menuItems: MenuItem[] = [ { - description: translate('workspace.qbo.preferredExporter'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER.getRoute(policyID)), + description: translate('workspace.xero.preferredExporter'), + onPress: () => {}, brickRoadIndicator: errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: exportConfiguration?.exporter ?? policyOwner, pendingAction: pendingFields?.export, error: errorFields?.exporter ? translate('common.genericErrorMessage') : undefined, }, { - description: translate('workspace.qbo.date'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT.getRoute(policyID)), - brickRoadIndicator: errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: exportDate ? translate(`workspace.qbo.exportDate.values.${exportDate}.label`) : undefined, - pendingAction: pendingFields?.exportDate, - error: errorFields?.exportDate ? translate('common.genericErrorMessage') : undefined, - }, - { - description: translate('workspace.qbo.exportExpenses'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES.getRoute(policyID)), - brickRoadIndicator: Boolean(errorFields?.exportEntity) || Boolean(errorFields?.reimbursableExpensesAccount) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: reimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${reimbursableExpensesExportDestination}`) : undefined, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - pendingAction: pendingFields?.reimbursableExpensesExportDestination || pendingFields?.reimbursableExpensesAccount, + description: translate('workspace.xero.exportExpenses'), + title: translate('workspace.xero.purchaseBill'), + interactive: false, + shouldShowRightIcon: false, + helperText: translate('workspace.xero.exportExpensesDescription'), }, { - description: translate('workspace.qbo.exportInvoices'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT.getRoute(policyID)), - brickRoadIndicator: errorFields?.receivableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: receivableAccount?.name, - pendingAction: pendingFields?.receivableAccount, - error: errorFields?.receivableAccount ? translate('common.genericErrorMessage') : undefined, + description: translate('workspace.xero.purchaseBillDate'), + onPress: () => {}, + brickRoadIndicator: errorFields?.billDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportConfiguration?.billDate, + pendingAction: pendingFields?.export, + error: errorFields?.billDate ? translate('common.genericErrorMessage') : undefined, }, { - description: translate('workspace.qbo.exportCompany'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT.getRoute(policyID)), - brickRoadIndicator: errorFields?.exportCompanyCard ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: nonReimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${nonReimbursableExpensesExportDestination}`) : undefined, - pendingAction: pendingFields?.nonReimbursableExpensesExportDestination, - error: errorFields?.nonReimbursableExpensesExportDestination ? translate('common.genericErrorMessage') : undefined, + description: translate('workspace.xero.exportInvoices'), + title: translate('workspace.xero.salesInvoice'), + interactive: false, + shouldShowRightIcon: false, + helperText: translate('workspace.xero.exportInvoicesDescription'), }, { - description: translate('workspace.qbo.exportExpensifyCard'), - title: translate('workspace.qbo.accounts.credit_card'), + description: translate('workspace.xero.exportCompanyCard'), + title: translate('workspace.xero.bankTransactions'), shouldShowRightIcon: false, interactive: false, + helperText: translate('workspace.xero.exportDeepDiveCompanyCard'), + }, + { + description: translate('workspace.xero.xeroBankAccount'), + onPress: () => {}, + brickRoadIndicator: errorFields?.nonReimbursableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: undefined, + pendingAction: pendingFields?.export, + error: undefined, }, ]; @@ -93,9 +80,9 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { includeSafeAreaPaddingBottom={false} testID={XeroExportConfigurationPage.displayName} > - + - {translate('workspace.qbo.exportDescription')} + {translate('workspace.xero.exportDescription')} {menuItems.map((menuItem) => ( ))} - - {`${translate('workspace.qbo.deepDiveExpensifyCard')} `} - Link.openExternalLink(CONST.DEEP_DIVE_EXPENSIFY_CARD)} - style={[styles.mutedNormalTextLabel, styles.link]} - > - {translate('workspace.qbo.deepDiveExpensifyCardIntegration')} - - From 2ef017b1c7d2ef0f8f0304323c71e55986b75bf1 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 11:51:31 +0530 Subject: [PATCH 44/81] link to export page --- .../accounting/xero/import/XeroCustomerConfigurationPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx index 45d0a2a4ad1e..62d33294c888 100644 --- a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx @@ -29,6 +29,7 @@ function XeroCustomerConfigurationPage({policy}: WithPolicyProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={styles.ph5} > From 6fc8538e5f44aa88607abd72b8f13ac43f3f4a13 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 12:06:52 +0530 Subject: [PATCH 45/81] fix type --- .../accounting/xero/import/XeroCustomerConfigurationPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx index 62d33294c888..45d0a2a4ad1e 100644 --- a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx @@ -29,7 +29,6 @@ function XeroCustomerConfigurationPage({policy}: WithPolicyProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - contentContainerStyle={styles.ph5} > From 6633fd824b2894ee1d793ee5083f7e410c9425d5 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 12:08:48 +0530 Subject: [PATCH 46/81] add comment --- src/components/MenuItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 1f0093c3c72b..42de7e2fb7f4 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -144,6 +144,7 @@ type MenuItemBaseProps = { /** Text to show below menu item. This text is not interactive */ helperText?: string; + /** Any additional styles to pass to helper text. */ helperTextStyle?: StyleProp; /** Should the description be shown above the title (instead of the other way around) */ From 7925d957d9d6bfbd3d9144d6b4ecdf22ec9f07a8 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 16:18:54 +0530 Subject: [PATCH 47/81] use conenction layput --- ios/NewExpensify.xcodeproj/project.pbxproj | 36 ++--------- src/components/ConnectionLayout.tsx | 42 ++++++++++--- src/libs/Permissions.ts | 2 +- .../xero/advanced/XeroAdvancedPage.tsx | 1 + .../export/XeroExportConfigurationPage.tsx | 60 ++++++++----------- .../import/XeroCustomerConfigurationPage.tsx | 1 + 6 files changed, 68 insertions(+), 74 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 9a9ca9c7dcbb..a3e667397bf0 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -1555,11 +1555,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1627,11 +1623,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -1709,11 +1701,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1858,11 +1846,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1999,11 +1983,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2138,11 +2118,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 3aaf84121fe7..b3f76aa21d47 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {View} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,23 +15,51 @@ import Text from './Text'; type ConnectionLayoutProps = { /** Used to set the testID for tests */ displayName: string; + /** Header title for the connection */ headerTitle: TranslationPaths; + /** React nodes that will be shown */ children?: React.ReactNode; + /** Title of the connection component */ title?: TranslationPaths; + /** Subtitle of the connection */ subtitle?: TranslationPaths; + /** The current policyID */ policyID: string; + /** Defines which types of access should be verified */ accessVariants?: PolicyAccessVariant[]; + /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; + + /** The content container style of Scrollview */ + contentContainerStyle?: StyleProp | undefined; + + /** Style of the title text */ + titleStyle?: StyleProp | undefined; + + /** Style of the subtitle text */ + subTitleStyle?: StyleProp | undefined; }; -function ConnectionLayout({displayName, headerTitle, children, title, subtitle, policyID, accessVariants, featureName}: ConnectionLayoutProps) { +function ConnectionLayout({ + displayName, + headerTitle, + children, + title, + subtitle, + policyID, + accessVariants, + featureName, + contentContainerStyle, + titleStyle, + subTitleStyle, +}: ConnectionLayoutProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -50,13 +78,9 @@ function ConnectionLayout({displayName, headerTitle, children, title, subtitle, title={translate(headerTitle)} onBackButtonPress={() => Navigation.goBack()} /> - - {title && ( - - {translate(title)} - - )} - {subtitle && {translate(subtitle)}} + + {title && {translate(title)}} + {subtitle && {translate(subtitle)}} {children} diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 79955c0fdf30..1675a230a1c3 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.ALL); + return true; } function canUseChronos(betas: OnyxEntry): boolean { diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index f781769adabf..87892ce60cef 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -28,6 +28,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={[styles.pb2, styles.ph5]} > - - - - {translate('workspace.xero.exportDescription')} - {menuItems.map((menuItem) => ( - - - - ))} - - - + {menuItems.map((menuItem) => ( + + + + ))} + ); } diff --git a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx index 45d0a2a4ad1e..1d1cf11f9791 100644 --- a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx @@ -29,6 +29,7 @@ function XeroCustomerConfigurationPage({policy}: WithPolicyProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={[[styles.pb2, styles.ph5]]} > From 5d75bcd02e3791e6807d171fd95cd05e1364e424 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 3 May 2024 18:11:42 +0700 Subject: [PATCH 48/81] fix: iou link in thread is wrong --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 013bc484fc63..105eadffd436 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -396,9 +396,10 @@ const ContextMenuActions: ContextMenuAction[] = [ return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); Environment.getEnvironmentURL().then((environmentURL) => { const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${reportID}/${reportActionID}`); + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); }); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, From 1bd9350c3f93fb5fa91475f908815d157c62f616 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 18:30:25 +0530 Subject: [PATCH 49/81] move set status to exports page --- src/languages/en.ts | 11 +++++------ src/languages/es.ts | 9 ++++----- .../accounting/xero/advanced/XeroAdvancedPage.tsx | 10 ---------- .../xero/export/XeroExportConfigurationPage.tsx | 9 ++++++++- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index aec8c79abad0..c616c0d8d64c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2004,16 +2004,15 @@ export default { export: 'Export', exportDescription: 'Configure how data in Expensify gets exported to Xero.', exportCompanyCard: 'Export company card expenses as', - purchaseBill: 'Purchase Bill', + purchaseBill: 'Purchase bill', exportDeepDiveCompanyCard: 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', bankTransactions: 'Bank transactions', - xeroBankAccount: 'Xero Bank Account', + xeroBankAccount: 'Xero bank account', preferredExporter: 'Preferred exporter', exportExpenses: 'Export out-of-pocket expenses as', - exportExpensesDescription: - 'Reports will export as a Purchase Bill awaiting payment, posting on the last day of the month in which expenses were incurred. This is the only export option with Xero.', - purchaseBillDate: 'Purchase Bill Date', + exportExpensesDescription: 'Reports will export as a purchase bill using the date and with the status you select below.', + purchaseBillDate: 'Purchase bill date', exportInvoices: 'Export invoices as', salesInvoice: 'Sales invoice', exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', @@ -2021,7 +2020,7 @@ export default { advanced: 'Advanced', autoSync: 'Auto-Sync', autoSyncDescription: 'Sync Xero and Expensify automatically every day.', - purchaseBillStatusTitle: 'Set purchase bill status (optional)', + purchaseBillStatusTitle: 'Purchase bill status', reimbursedReports: 'Sync reimbursed reports', reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Xero account below.', xeroBillPaymentAccount: 'Xero Bill Payment Account', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7a66adc5e947..bc8ddb419158 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2037,16 +2037,15 @@ export default { export: 'Exportar', exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', exportCompanyCard: 'Export company card expenses as', - purchaseBill: 'Purchase Bill', + purchaseBill: 'Purchase bill', exportDeepDiveCompanyCard: 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', bankTransactions: 'Bank transactions', xeroBankAccount: 'Xero días laborales', preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos de bolsillo como', - exportExpensesDescription: - 'Reports will export as a Purchase Bill awaiting payment, posting on the last day of the month in which expenses were incurred. This is the only export option with Xero.', - purchaseBillDate: 'Purchase Bill Date', + exportExpensesDescription: 'Reports will export as a purchase bill using the date and with the status you select below.', + purchaseBillDate: 'Purchase bill date', exportInvoices: 'Exportar facturas como', salesInvoice: 'Sales invoice', exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', @@ -2054,7 +2053,7 @@ export default { advanced: 'Avanzado', autoSync: 'Autosincronización', autoSyncDescription: 'Sincroniza Xero y Expensify automáticamente todos los días.', - purchaseBillStatusTitle: 'Set purchase bill status (optional)', + purchaseBillStatusTitle: 'Purchase bill status', reimbursedReports: 'Sincronizar informes reembolsados', reimbursedReportsDescription: 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.', diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index 87892ce60cef..231e9c3ec54a 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -46,16 +46,6 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { errors={ErrorUtils.getLatestErrorField(xeroConfig ?? {}, CONST.XERO_CONFIG.AUTO_SYNC)} onCloseError={() => Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.AUTO_SYNC)} /> - - {}} - /> - {}, brickRoadIndicator: errorFields?.billDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: exportConfiguration?.billDate, pendingAction: pendingFields?.export, error: errorFields?.billDate ? translate('common.genericErrorMessage') : undefined, }, + { + description: translate('workspace.xero.advancedConfig.purchaseBillStatusTitle'), + onPress: () => {}, + title: exportConfiguration?.billStatus.purchase, + pendingAction: pendingFields?.export, + error: errorFields?.purchase ? translate('common.genericErrorMessage') : undefined, + }, { description: translate('workspace.xero.exportInvoices'), title: translate('workspace.xero.salesInvoice'), From a28cc87a9c5b83215bd5d95625e987d3692025a9 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 18:37:02 +0530 Subject: [PATCH 50/81] revert proj changes --- ios/NewExpensify.xcodeproj/project.pbxproj | 38 ++++++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index a3e667397bf0..d4eb9ef3b0ff 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -1555,7 +1555,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1623,7 +1627,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -1701,7 +1709,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1846,7 +1858,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1983,7 +1999,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2118,7 +2138,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2251,4 +2275,4 @@ /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; -} +} \ No newline at end of file From ee8c920f7314cfcf60b4f4e5759788f0fba6bdb7 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 18:37:25 +0530 Subject: [PATCH 51/81] revert proj changes --- ios/NewExpensify.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index d4eb9ef3b0ff..9a9ca9c7dcbb 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -2275,4 +2275,4 @@ /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; -} \ No newline at end of file +} From 783ff95658f8177645733176dab5917a53e03e07 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 3 May 2024 18:53:33 +0530 Subject: [PATCH 52/81] revert --- src/libs/Permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1675a230a1c3..79955c0fdf30 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; + return !!betas?.includes(CONST.BETAS.ALL); } function canUseChronos(betas: OnyxEntry): boolean { From 89e8b48f6dec39ae9c60d6a34aaf38d49a84116d Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Fri, 3 May 2024 13:25:59 -0400 Subject: [PATCH 53/81] Create Connect-a-Business-Bank-Account.md --- .../Connect-a-Business-Bank-Account.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md new file mode 100644 index 000000000000..8cf0a18ba529 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md @@ -0,0 +1,98 @@ +--- +title: Connect a Business Bank Account +description: How to connect a business bank account to New Expensify +--- +
+ +Adding a business bank account unlocks a myriad of features and automation in Expensify, such as: +- Reimburse expenses via direct bank transfer +- Pay bills +- Collect invoice payments +- Issue the Expensify Card + +# To connect a bank account +1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account > Connect bank account** +2. Click **Connect online with Plaid** +3. Click **Continue** +4. When you reach the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access +5. Login to the business bank account: +- If the bank is not listed, click the X to go back to the connection type +- Here you’ll see the option to **Connect Manually** +- Enter your account and routing numbers +6. Enter your bank login credentials: +- If your bank requires additional security measures, you will be directed to obtain and enter a security code +- If you have more than one account available to choose from, you will be directed to choose the desired account + +## Enter company information +This is where you’ll add the legal business name as well as several other company details. + +- **Company address**: The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) +- **Tax Identification Number**: This is the identification number that was assigned to the business by the IRS +- **Company website**: A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com +- **Industry Classification Code**: You can locate a list of Industry Classification Codes [here]([url](https://www.census.gov/naics/?input=software&year=2022)) + +## Enter personal information +Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: +- The address must be a physical address +- The address must be located in the US +- The SSN must be US-issued + +This does not need to be a signor on the bank account. If someone other than the Expensify account owner enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. + +## Upload ID +After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: +1. Upload a photo of the front and back of your ID (this cannot be a photo of an existing image) +2. Use your device to take a selfie and record a short video of yourself + +**Your ID must be:** +- Issued in the US +- Current (ie: the expiration date must be in the future) + +## Additional Information +Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: +- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. +- If you or another **individual** owns 25% or more of the business, please check the appropriate box +- If someone else owns 25% or more of the business, you will be prompted to provide their personal information + +If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. + +The details you submitted may require additional review. If that's the case, you'll receive a message from the Concierge outlining the next steps. Otherwise, your bank account will be connected automatically. + +{% include faq-begin.md %} + +## What are the general requirements for adding a business bank account? + +To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US), or to issue Expensify Cards: +- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. +If you are adding the bank account to Expensify, you must do so from your Expensify account settings. +- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. For using features related to US ACH, your ID must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address +- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. + +## What is a Beneficial Owner? + +A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. + +## What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? + +Please indicate you have a Beneficial Owner only if it is an individual who owns 25% or more of the business. + +## Why can’t I input my address or upload my ID? + +Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account and then share access with you once it is verified. + +## Why am I asked for documents when adding my bank account? + +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. + +If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. + +## I don’t see all three microtransactions I need to validate my bank account. What should I do? + +It's a good idea to wait until the end of that second business day. If you still don’t see them, please contact your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify." + +Once that's all set, make sure to contact your account manager or concierge, and our team will be able to re-trigger those three test transactions! + +{% include faq-end.md %} + +
From db5ca03df014b73f851b22c208c8668477fd90fd Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 4 May 2024 23:15:04 +0300 Subject: [PATCH 54/81] Update en.ts --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c616c0d8d64c..ea8b05d8cd49 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2011,7 +2011,7 @@ export default { xeroBankAccount: 'Xero bank account', preferredExporter: 'Preferred exporter', exportExpenses: 'Export out-of-pocket expenses as', - exportExpensesDescription: 'Reports will export as a purchase bill using the date and with the status you select below.', + exportExpensesDescription: 'Reports will export as a purchase bill, using the date and status you select below.', purchaseBillDate: 'Purchase bill date', exportInvoices: 'Export invoices as', salesInvoice: 'Sales invoice', From fc7df473571a2718d2827a134a13eba615f1b190 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 6 May 2024 16:17:15 +0700 Subject: [PATCH 55/81] rename event --- src/libs/actions/Report.ts | 2 +- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d413b15f67aa..a20dde56b4aa 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1253,7 +1253,7 @@ function handleReportChanged(report: OnyxEntry) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); }; } - DeviceEventEmitter.emit(`switchToCurrentReport_${report.reportID}`, { + DeviceEventEmitter.emit(`switchToPreExistingReport_${report.reportID}`, { preexistingReportID: report.preexistingReportID, callback, }); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 5ae49c322749..3120bbe9bed2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -345,7 +345,7 @@ function ComposerWithSuggestions( ); useEffect(() => { - const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToCurrentReport_${reportID}`, ({preexistingReportID, callback}) => { + const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToPreExistingReport_${reportID}`, ({preexistingReportID, callback}) => { if (!commentRef.current) { callback(); return; From 03fbbbe6ee704329c33900bffed7edcd5c4650db Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:13 +0300 Subject: [PATCH 56/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index bc8ddb419158..3f5b38e90d7c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2036,7 +2036,7 @@ export default { notImported: 'No importado', export: 'Exportar', exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', - exportCompanyCard: 'Export company card expenses as', + exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como', purchaseBill: 'Purchase bill', exportDeepDiveCompanyCard: 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', From b7953fd2824c03f05ad2c69be28a883adb8a5ae9 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:26 +0300 Subject: [PATCH 57/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 3f5b38e90d7c..31fe1e354365 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2039,7 +2039,7 @@ export default { exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como', purchaseBill: 'Purchase bill', exportDeepDiveCompanyCard: - 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', + 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', bankTransactions: 'Bank transactions', xeroBankAccount: 'Xero días laborales', preferredExporter: 'Exportador preferido', From e4cf440a2e52317247d2641eace35f5e3cfa1686 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:32 +0300 Subject: [PATCH 58/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 31fe1e354365..2b7fbd164c8d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2040,7 +2040,7 @@ export default { purchaseBill: 'Purchase bill', exportDeepDiveCompanyCard: 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', - bankTransactions: 'Bank transactions', + bankTransactions: 'Transacciones bancarias', xeroBankAccount: 'Xero días laborales', preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos de bolsillo como', From 7e88d91da62d8c9b6801bb1dc2f1f95d2ef33ba8 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:43 +0300 Subject: [PATCH 59/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2b7fbd164c8d..88e071377ca9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2043,7 +2043,7 @@ export default { bankTransactions: 'Transacciones bancarias', xeroBankAccount: 'Xero días laborales', preferredExporter: 'Exportador preferido', - exportExpenses: 'Exportar gastos de bolsillo como', + exportExpenses: 'Exportar gastos por cuenta propia como', exportExpensesDescription: 'Reports will export as a purchase bill using the date and with the status you select below.', purchaseBillDate: 'Purchase bill date', exportInvoices: 'Exportar facturas como', From 5477528a4897d538b19909623a2380512b72d951 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:49 +0300 Subject: [PATCH 60/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 88e071377ca9..27859ae4b715 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2044,7 +2044,7 @@ export default { xeroBankAccount: 'Xero días laborales', preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos por cuenta propia como', - exportExpensesDescription: 'Reports will export as a purchase bill using the date and with the status you select below.', + exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', purchaseBillDate: 'Purchase bill date', exportInvoices: 'Exportar facturas como', salesInvoice: 'Sales invoice', From 840111c5652b3e691c220aec1c48e033342a29ca Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:07:54 +0300 Subject: [PATCH 61/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 27859ae4b715..84a5e9659e9b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2048,7 +2048,7 @@ export default { purchaseBillDate: 'Purchase bill date', exportInvoices: 'Exportar facturas como', salesInvoice: 'Sales invoice', - exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', + exportInvoicesDescription: 'Las facturas de venta siempre muestran la fecha en la que se envió la factura.', advancedConfig: { advanced: 'Avanzado', autoSync: 'Autosincronización', From 666c1eeff77c143e6aba9c9e49dbe6d67dcea11c Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:01 +0300 Subject: [PATCH 62/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 84a5e9659e9b..63dc7191a351 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2053,7 +2053,7 @@ export default { advanced: 'Avanzado', autoSync: 'Autosincronización', autoSyncDescription: 'Sincroniza Xero y Expensify automáticamente todos los días.', - purchaseBillStatusTitle: 'Purchase bill status', + purchaseBillStatusTitle: 'Estado de la factura de compra', reimbursedReports: 'Sincronizar informes reembolsados', reimbursedReportsDescription: 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.', From e42a8f670bf66f2e3fe4b824f7655437e4611a0a Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:15 +0300 Subject: [PATCH 63/81] Update src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx Co-authored-by: Hans --- .../accounting/xero/export/XeroExportConfigurationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx index 962804969ff8..934c41dab614 100644 --- a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -45,7 +45,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { { description: translate('workspace.xero.advancedConfig.purchaseBillStatusTitle'), onPress: () => {}, - title: exportConfiguration?.billStatus.purchase, + title: exportConfiguration?.billStatus?.purchase, pendingAction: pendingFields?.export, error: errorFields?.purchase ? translate('common.genericErrorMessage') : undefined, }, From b98c1cca006156d71ab113564107f570cdac0fb3 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:25 +0300 Subject: [PATCH 64/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 63dc7191a351..54907a1ca4ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2047,7 +2047,7 @@ export default { exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', purchaseBillDate: 'Purchase bill date', exportInvoices: 'Exportar facturas como', - salesInvoice: 'Sales invoice', + salesInvoice: 'Factura de venta', exportInvoicesDescription: 'Las facturas de venta siempre muestran la fecha en la que se envió la factura.', advancedConfig: { advanced: 'Avanzado', From a95e49c14847f103d169fca6dfb780b56a4af855 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:34 +0300 Subject: [PATCH 65/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 54907a1ca4ad..1ddac3e41325 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2045,7 +2045,7 @@ export default { preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos por cuenta propia como', exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', - purchaseBillDate: 'Purchase bill date', + purchaseBillDate: 'Fecha de la factura de compra', exportInvoices: 'Exportar facturas como', salesInvoice: 'Factura de venta', exportInvoicesDescription: 'Las facturas de venta siempre muestran la fecha en la que se envió la factura.', From 94198a32f39f2dc659a932853cd0956f2b3f4d5b Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:41 +0300 Subject: [PATCH 66/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 1ddac3e41325..b0211fde9253 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2041,7 +2041,7 @@ export default { exportDeepDiveCompanyCard: 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', bankTransactions: 'Transacciones bancarias', - xeroBankAccount: 'Xero días laborales', + xeroBankAccount: 'Cuenta bancaria de Xero', preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos por cuenta propia como', exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', From 61c111d3b2a27ba5b5c23756d46dbb6ab62f6ef0 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 15:08:48 +0300 Subject: [PATCH 67/81] Update src/languages/es.ts Co-authored-by: Rocio Perez-Cano --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index b0211fde9253..dbc435ffa4f4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2037,7 +2037,7 @@ export default { export: 'Exportar', exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como', - purchaseBill: 'Purchase bill', + purchaseBill: 'Factura de compra', exportDeepDiveCompanyCard: 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', bankTransactions: 'Transacciones bancarias', From 1d9a8802bd1c4823465d097196375660e678e313 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 22:28:07 +0530 Subject: [PATCH 68/81] rm hub --- docs/_data/_routes.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index a44d7d11af87..f196f53a47f5 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -114,11 +114,6 @@ platforms: icon: /assets/images/money-into-wallet.svg description: Learn more about expense tracking and submission. - - href: bank-accounts-and-payments - title: Bank Accounts & Payments - icon: /assets/images/bank-card.svg - description: Send direct reimbursements, pay invoices, and receive payment. - - href: expensify-card title: Expensify Card icon: /assets/images/hand-card.svg From a61c6ad3650e12d78fa906c5cfc36e87026338af Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 22:28:17 +0530 Subject: [PATCH 69/81] rm article from hub --- .../Connect-a-Bank-Account.md | 151 ------------------ 1 file changed, 151 deletions(-) delete mode 100644 docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md diff --git a/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md b/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md deleted file mode 100644 index bc0676231544..000000000000 --- a/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Connect a Business Bank Account - US -description: How to connect a business bank account to Expensify (US) ---- -# Overview -Adding a verified business bank account unlocks a myriad of features and automation in Expensify. -Once you connect your business bank account, you can: -- Reimburse expenses via direct bank transfer -- Pay bills -- Collect invoice payments -- Issue the Expensify Card - -# How to add a verified business bank account -To connect a business bank account to Expensify, follow the below steps: -1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account > Connect bank account** -2. Click **Connect online with Plaid** -3. Click **Continue** -4. When you reach the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access -5. Login to the business bank account: -- If the bank is not listed, click the X to go back to the connection type -- Here you’ll see the option to **Connect Manually** -- Enter your account and routing numbers -6. Enter your bank login credentials: -- If your bank requires additional security measures, you will be directed to obtain and enter a security code -- If you have more than one account available to choose from, you will be directed to choose the desired account - -Next, to verify the bank account, you’ll enter some details about the business as well as some personal information. - -## Enter company information -This is where you’ll add the legal business name as well as several other company details. - -- **Company address**: The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) -- **Tax Identification Number**: This is the identification number that was assigned to the business by the IRS -- **Company website**: A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com -- **Industry Classification Code**: You can locate a list of Industry Classification Codes [here]([url](https://www.census.gov/naics/?input=software&year=2022)) - -## Enter personal information -Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: -- The address must be a physical address -- The address must be located in the US -- The SSN must be US-issued - -This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. - -## Upload ID -After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: -1. Upload a photo of the front and back of your ID (this cannot be a photo of an existing image) -2. Use your device to take a selfie and record a short video of yourself - -**Your ID must be:** -- Issued in the US -- Current (ie: the expiration date must be in the future) - -## Additional Information -Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: -- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. -- If you or another **individual** owns 25% or more of the business, please check the appropriate box -- If someone else owns 25% or more of the business, you will be prompted to provide their personal information - -If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. - -# How to validate the bank account - -The account you set up can be found under **Settings > Workspaces > _Workspace Name_ > Bank account** in either the **Verifying** or **Pending** state. - -If it is **Verifying**, then this means we sent you a message and need more information from you. Please review the automated message sent by Concierge. This should include a message with specific details about what's required to move forward. - -If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. If after two business days you do not see these test transactions, reach out to Concierge for assistance. - -After these transactions (2 withdrawals and 1 deposit) have been processed to your account, head to the **Bank accounts** section of your workspace settings. Here you'll see a prompt to input the transaction amounts. - -Once you've finished these steps, your business bank account is ready to use in Expensify! - -# How to delete a verified bank account -If you need to delete a bank account from Expensify, run through the following steps: -1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account** -2. Click the red **Delete** button under the corresponding bank account - -# Deep Dive - -## Verified bank account requirements - -To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US), or to issue Expensify Cards: -- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. -- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings. -- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. For using features related to US ACH, your ID must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address -- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. - -## Locked bank account -When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. - -Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. -If you need to enable direct debits from your verified bank account, your bank will require the following details: -- The ACH CompanyIDs (1270239450, 4270239450 and 2270239450) -- The ACH Originator Name (Expensify) - -If using Expensify to process Bill payments, you'll also need to whitelist the ACH IDs from our partner [Stripe](https://support.stripe.com/questions/ach-direct-debit-company-ids-for-stripe?): -- The ACH CompanyIDs (1800948598 and 4270465600) -- The ACH Originator Name (expensify.com) - -If using Expensify to process international reimbursements from your USD bank account, you'll also need to whitelist the ACH IDs from our partner CorPay: -- The ACH CompanyIDs (1522304924 and 2522304924) -- The ACH Originator Name (Cambridge Global Payments) - -To request to unlock the bank account, go to **Settings > Workspaces > _Workspace Name_ > Bank account** and click **Fix.** This sends a request to our support team to review why the bank account was locked, who will send you a message to confirm that. - -Unlocking a bank account can take 4-5 business days to process, to allow for ACH processing time and clawback periods. - -## Error adding an ID to Onfido - -Expensify is required by both our sponsor bank and federal law to verify the identity of the individual who is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else. - -If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps: - -1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. -2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" -3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). -4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. -5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. -6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. -7. If possible, try these steps on another device -8. If you have another phone available, try to follow these steps on that device -If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. - -{% include faq-begin.md %} -## What is a Beneficial Owner? - -A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. - -## What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? - -Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business. - -## Why can’t I input my address or upload my ID? - -Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified. - -## Why am I asked for documents when adding my bank account? - -When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. -If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. - -If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. - -## I don’t see all three microtransactions I need to validate my bank account. What should I do? - -It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify". - -Make sure to reach out to your Account Manager or Concierge once that's all set, and our team will be able to re-trigger those three test transactions! - -{% include faq-end.md %} From ad78eccb9fb8e74daf3232d83730c7a9cdec5e3c Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 6 May 2024 22:29:08 +0530 Subject: [PATCH 70/81] rm hub --- .../hubs/bank-accounts-and-payments/index.html | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 docs/new-expensify/hubs/bank-accounts-and-payments/index.html diff --git a/docs/new-expensify/hubs/bank-accounts-and-payments/index.html b/docs/new-expensify/hubs/bank-accounts-and-payments/index.html deleted file mode 100644 index 94db3c798710..000000000000 --- a/docs/new-expensify/hubs/bank-accounts-and-payments/index.html +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: default -title: Bank Accounts & Payments ---- - -{% include hub.html %} \ No newline at end of file From 0eb322fe9dd6e914cdb91a9cf2b8f7dafe04501e Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:25:33 +0530 Subject: [PATCH 71/81] add travel hub --- docs/_data/_routes.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index a44d7d11af87..60b6aa8e59e1 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -54,6 +54,11 @@ platforms: icon: /assets/images/hand-card.svg description: Explore the perks and benefits of the Expensify Card. + - href: travel + title: Travel + icon: /assets/images/plane.svg + description: Manage all your corporate travel needs with Expensify Travel. + - href: copilots-and-delegates title: Copilots & Delegates icon: /assets/images/envelope-receipt.svg @@ -124,6 +129,11 @@ platforms: icon: /assets/images/hand-card.svg description: Explore the perks and benefits of the Expensify Card. + - href: travel + title: Travel + icon: /assets/images/plane.svg + description: Manage all your corporate travel needs with Expensify Travel. + - href: connections title: Connections icon: /assets/images/workflow.svg From a1dd1b1da0f239ce8cea2aaadaf3e8e7ee25e8ac Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:25:40 +0530 Subject: [PATCH 72/81] add travel hub --- docs/articles/expensify-classic/travel/Coming-Soon.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/articles/expensify-classic/travel/Coming-Soon.md diff --git a/docs/articles/expensify-classic/travel/Coming-Soon.md b/docs/articles/expensify-classic/travel/Coming-Soon.md new file mode 100644 index 000000000000..4d32487a14b5 --- /dev/null +++ b/docs/articles/expensify-classic/travel/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming soon +description: Coming soon +--- + +# Coming soon \ No newline at end of file From e1876ee519a3fc15e67559842364090b3395633b Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:25:49 +0530 Subject: [PATCH 73/81] add travel hub --- docs/articles/new-expensify/travel/Coming-Soon.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/articles/new-expensify/travel/Coming-Soon.md diff --git a/docs/articles/new-expensify/travel/Coming-Soon.md b/docs/articles/new-expensify/travel/Coming-Soon.md new file mode 100644 index 000000000000..4d32487a14b5 --- /dev/null +++ b/docs/articles/new-expensify/travel/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming soon +description: Coming soon +--- + +# Coming soon \ No newline at end of file From 803919c6e21ebb611970ac00642a4143159f87e3 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:26:01 +0530 Subject: [PATCH 74/81] add plane illustration --- docs/assets/images/plane.svg | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/assets/images/plane.svg diff --git a/docs/assets/images/plane.svg b/docs/assets/images/plane.svg new file mode 100644 index 000000000000..0295aa3c66c0 --- /dev/null +++ b/docs/assets/images/plane.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6ab4b8d16f8bd5018c8909f0f2d907e2c78f3f0d Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:26:11 +0530 Subject: [PATCH 75/81] add travel hub --- docs/expensify-classic/hubs/travel/index.html | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/expensify-classic/hubs/travel/index.html diff --git a/docs/expensify-classic/hubs/travel/index.html b/docs/expensify-classic/hubs/travel/index.html new file mode 100644 index 000000000000..7c8c3d363d5e --- /dev/null +++ b/docs/expensify-classic/hubs/travel/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Travel +--- + +{% include hub.html %} From d30026740fa01b7b042ce2668672031106f4f9c2 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 7 May 2024 00:26:16 +0530 Subject: [PATCH 76/81] add travel hub --- docs/new-expensify/hubs/travel/index.html | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/new-expensify/hubs/travel/index.html diff --git a/docs/new-expensify/hubs/travel/index.html b/docs/new-expensify/hubs/travel/index.html new file mode 100644 index 000000000000..7c8c3d363d5e --- /dev/null +++ b/docs/new-expensify/hubs/travel/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Travel +--- + +{% include hub.html %} From a0f0c2bf845f36f004924cf2c6833d60b633174e Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 6 May 2024 20:10:23 -0500 Subject: [PATCH 77/81] DOCS: Create Enable-Two-Factor-Authentication.md New article --- .../Enable-Two-Factor-Authentication.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md diff --git a/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md b/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md new file mode 100644 index 000000000000..5f0a33cc8754 --- /dev/null +++ b/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md @@ -0,0 +1,51 @@ +--- +title: Enable Two-Factor Authentication (2FA) +description: Add an extra layer of security for your Expensify login +--- +
+ +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication (2FA). This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. + +To enable 2FA, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Security** in the left menu. +3. Under Security Options, click **Two Factor Authentication**. +4. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes. + - Click **Download** to save a copy of your backup codes to your computer. + - Click **Copy** to paste the codes into a document or other secure location. +5. Click **Next**. +6. Download or open your authenticator app and connect it to Expensify by either: + - Scanning the QR code + - Entering the code into your authenticator app +7. Enter the 6-digit code from your authenticator app into Expensify and click **Verify**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Security**. +3. Under Security Options, tap **Two Factor Authentication**. +4. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes. + - Tap **Download** to save a copy of your backup codes to your device. + - Tap **Copy** to paste the codes into a document or other secure location. +5. Tap **Next**. +6. Download or open your authenticator app and connect it to Expensify by either: + - Scanning the QR code + - Entering the code into your authenticator app +7. Enter the 6-digit code from your authenticator app into Expensify and tap **Verify**. +{% include end-option.html %} + +{% include end-selector.html %} + +When you log in to Expensify in the future, you’ll be emailed a magic code that you’ll use to log in with. Then you’ll be prompted to open your authenticator app to get the 6-digit code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. + +{% include faq-begin.md %} +**How do I use my recovery codes if I lose access to my authenticator app?** + +Your recovery codes work the same way as your authenticator codes. Just enter a recovery code as you would the authenticator code. +{% include faq-end.md %} + +
From 6fb091533ba8c737431512831cae925c5ea8ab41 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 6 May 2024 20:19:44 -0500 Subject: [PATCH 78/81] DOCS: Create Update-Notification-Preferences.md New article --- .../Update-Notification-Preferences.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/articles/new-expensify/settings/Update-Notification-Preferences.md diff --git a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md new file mode 100644 index 000000000000..e4111b3d02d3 --- /dev/null +++ b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md @@ -0,0 +1,29 @@ +--- +title: Update notification preferences +description: Determine how you want to receive Expensify notifications +--- +
+ +To customize the email and in-app notifications you receive from Expensify, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Enable or disable the toggles under Notifications: + - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. + - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Enable or disable the toggles under Notifications: + - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. + - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. +{% include end-option.html %} + +{% include end-selector.html %} + +
From e418d87d421dd2b9628930e2f847886cde5b5992 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 6 May 2024 20:23:56 -0500 Subject: [PATCH 79/81] DOCS: Create Switch-account-language-to-Spanish.md New article --- .../Switch-account-language-to-Spanish.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md diff --git a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md new file mode 100644 index 000000000000..a431d34fbc0f --- /dev/null +++ b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md @@ -0,0 +1,23 @@ +--- +title: Switch account language to Spanish +description: Change your account language +--- +
+ +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Click the Language option and select **Spanish**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Tap the Language option and select **Spanish**. +{% include end-option.html %} + +{% include end-selector.html %} + +
From 7e82fe57e41fca5c71017a719f2e9fdd86f1cfcd Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 7 May 2024 11:49:43 +0700 Subject: [PATCH 80/81] show category picker in invoices confirmation --- .../MoneyRequestConfirmationList.tsx | 7 ++++--- src/libs/ReportUtils.ts | 1 + .../request/step/IOURequestStepCategory.tsx | 20 ++++++++++--------- .../step/IOURequestStepConfirmation.tsx | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b97578210ad9..f9e4e436b337 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -301,11 +301,12 @@ function MoneyRequestConfirmationList({ const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); - // A flag for showing the tags field - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); + // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 + const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, policyTagLists, isTypeInvoice]); // A flag for showing tax rate - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); + // TODO: remove the !isTypeInvoice from this condition after BE supports tax for invoices: https://github.com/Expensify/App/issues/41281 + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy) && !isTypeInvoice; // A flag for showing the billable field const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 641d3ddaa268..fb3371e94491 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6686,6 +6686,7 @@ export { isExpensifyOnlyParticipantInReport, isGroupChat, isGroupChatAdmin, + isGroupPolicy, isReportInGroupPolicy, isHoldCreator, isIOUOwnedByCurrentUser, diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 0bd546318186..d4919a4172aa 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -83,9 +83,11 @@ function IOURequestStepCategory({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; - // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = ReportUtils.isReportInGroupPolicy(report, policy) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = + (ReportUtils.isReportInGroupPolicy(report) || ReportUtils.isGroupPolicy(policy?.type ?? '')) && + // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); @@ -153,7 +155,7 @@ function IOURequestStepCategory({ {translate('iou.categorySelection')} @@ -170,19 +172,19 @@ const IOURequestStepCategoryWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyDraft: { - key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${reportDraft ? reportDraft.policyID : '0'}`, + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, }, policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyCategoriesDraft: { - key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${reportDraft ? reportDraft.policyID : '0'}`, + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, }, policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`, }, reportActions: { key: ({ diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 831d58c43434..e458e57ae3bb 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -146,7 +146,7 @@ function IOURequestStepConfirmation({ }) ?? [], [transaction?.participants, personalDetails, iouType], ); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); + const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)) || ReportUtils.isGroupPolicy(policy?.type ?? ''), [report, policy]); const formHasBeenSubmitted = useRef(false); useEffect(() => { @@ -574,7 +574,7 @@ function IOURequestStepConfirmation({ iouType={iouType} reportID={reportID} isPolicyExpenseChat={isPolicyExpenseChat} - policyID={report?.policyID} + policyID={report?.policyID ?? policy?.id} bankAccountRoute={ReportUtils.getBankAccountRoute(report)} iouMerchant={transaction?.merchant} iouCreated={transaction?.created} From 5efa8a36c818c5ee85a893f83b6ed483214a1751 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 7 May 2024 12:02:32 +0700 Subject: [PATCH 81/81] revert redundant changes --- src/components/MoneyRequestConfirmationList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index f9e4e436b337..cad1c6c1bcf4 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -301,6 +301,7 @@ function MoneyRequestConfirmationList({ const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); + // A flag for showing the tags field // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, policyTagLists, isTypeInvoice]);