From 0f63c8a1867407a9cc474866f6f18f06dd4328b1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 28 Feb 2024 14:55:53 +0530 Subject: [PATCH 001/617] 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 002/617] 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 003/617] 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 004/617] 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 005/617] 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 006/617] 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 007/617] 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 008/617] 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 009/617] 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 010/617] 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 011/617] 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 012/617] 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 013/617] 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 014/617] 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 015/617] 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 016/617] 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 017/617] 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 018/617] 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 019/617] 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 454c1786a89a1ea2190272a993a8ed414835c8f5 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:19:11 +0100 Subject: [PATCH 020/617] Fix: Gray Replies does not disappear together with reply when reply is deleted --- src/pages/home/report/ReportActionsList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index d1b9c420b0af..3d635603e10a 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -532,14 +532,13 @@ function ReportActionsList({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} - shouldDisplayReplyDivider={sortedReportActions.length > 1} + shouldDisplayReplyDivider={sortedVisibleReportActions.length > 1} /> ), [ report, linkedReportActionID, sortedVisibleReportActions, - sortedReportActions.length, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker, From ccbd4f6cf3c171a5ddef0e25ad1898ca06693d6b Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 1 Apr 2024 02:47:42 +0100 Subject: [PATCH 021/617] Thread - Invited thread member can click on Thread when they are not a member of main chat --- src/components/ParentNavigationSubtitle.tsx | 4 ++++ src/libs/ReportUtils.ts | 8 ++++++++ src/pages/home/report/ReportActionItem.tsx | 7 ++++++- src/pages/home/report/ReportActionItemParentAction.tsx | 3 ++- src/pages/home/report/ThreadDivider.tsx | 5 ++++- src/styles/utils/index.ts | 4 ++-- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 3109453ca6b0..a778e2a701db 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -25,6 +25,10 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID const {translate} = useLocalize(); + if (!reportName){ + return; + } + return ( { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9fa28535a7a7..652c99f2b491 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5306,6 +5306,13 @@ function isReportParticipant(accountID: number, report: OnyxEntry): bool return possibleAccountIDs.includes(accountID); } +/** + * Check to see if the current user has access to view the report. + */ +function canCurrentUserOpenReport(report: OnyxEntry): boolean { + return (isReportParticipant(currentUserAccountID ?? 0, report) || isPublicRoom(report)) && canAccessReport(report, allPolicies, allBetas); +} + function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } @@ -5857,6 +5864,7 @@ export { getChildReportNotificationPreference, getAllAncestorReportActions, isReportParticipant, + canCurrentUserOpenReport, isValidReport, getReportDescriptionText, isReportFieldOfTypeTitle, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 2716fedcf59a..e43f8fbbfc6b 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -145,6 +145,9 @@ type ReportActionItemProps = { /** Callback to be called on onPress */ onPress?: () => void; + + /** Should press be disabled */ + isDisabled?: boolean; } & ReportActionItemOnyxProps; const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU => @@ -169,6 +172,7 @@ function ReportActionItem({ policy, transaction, onPress = undefined, + isDisabled = false, }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -831,6 +835,7 @@ function ReportActionItem({ isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} @@ -860,7 +865,7 @@ function ReportActionItem({ checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} /> - + ReportActions.clearAllRelatedReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 3d98973c86c4..c55cae369410 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -101,9 +101,10 @@ function ReportActionItemParentAction({ errorRowStyles={[styles.ml10, styles.mr2]} onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > - + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? ''))} + isDisabled={!ReportUtils.canCurrentUserOpenReport(ReportUtils.getReport(ancestor?.report?.parentReportID) as OnyxTypes.Report)} parentReportAction={parentReportAction} report={ancestor.report} reportActions={reportActions} diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index 083129e15e6d..4500af526428 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -16,9 +16,11 @@ import ROUTES from '@src/ROUTES'; type ThreadDividerProps = { /** Thread ancestor */ ancestor: Ancestor; + /** Whether the link is disbled */ + isDisabled: boolean; }; -function ThreadDivider({ancestor}: ThreadDividerProps) { +function ThreadDivider({ancestor, isDisabled}: ThreadDividerProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -30,6 +32,7 @@ function ThreadDivider({ancestor}: ThreadDividerProps) { accessibilityLabel={translate('threads.thread')} role={CONST.ROLE.BUTTON} style={[styles.flexRow, styles.alignItemsCenter, styles.gap1]} + disabled={isDisabled} > ({ /** * Generate the styles for the ReportActionItem wrapper view. */ - getReportActionItemStyle: (isHovered = false, isClickable = false): ViewStyle => + getReportActionItemStyle: (isHovered = false, isClickable = false, isDisabled = false): ViewStyle => // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) // eslint-disable-next-line @typescript-eslint/no-unsafe-return ({ @@ -1462,7 +1462,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android theme.transparent, opacity: 1, - ...(isClickable ? styles.cursorPointer : styles.cursorInitial), + ...(isDisabled ? styles.cursorDisabled : (isClickable ? styles.cursorPointer : styles.cursorInitial)), }), /** From 2fc36ea9e2ea31ab8a4cb32135f003f9027c13f7 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 1 Apr 2024 02:48:36 +0100 Subject: [PATCH 022/617] Fix: Thread-Cursor is hand cursor instead of text cursor when hovering over edit composer in thread --- src/pages/home/report/ReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index e43f8fbbfc6b..732e7ce06e55 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -865,7 +865,7 @@ function ReportActionItem({ checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} /> - + ReportActions.clearAllRelatedReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing From 80078c34e96b189b79396e40999043155836f410 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 1 Apr 2024 02:51:19 +0100 Subject: [PATCH 023/617] prettier --- src/components/ParentNavigationSubtitle.tsx | 2 +- src/pages/home/report/ReportActionItem.tsx | 8 +++++++- src/pages/home/report/ReportActionItemParentAction.tsx | 5 ++++- src/styles/utils/index.ts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index a778e2a701db..24b2ddf8a10c 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -25,7 +25,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID const {translate} = useLocalize(); - if (!reportName){ + if (!reportName) { return; } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 732e7ce06e55..ebabb189a23d 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -865,7 +865,13 @@ function ReportActionItem({ checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} /> - + ReportActions.clearAllRelatedReportActionErrors(report.reportID, action)} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index c55cae369410..4bdbe842df96 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -101,7 +101,10 @@ function ReportActionItemParentAction({ errorRowStyles={[styles.ml10, styles.mr2]} onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > - + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? ''))} isDisabled={!ReportUtils.canCurrentUserOpenReport(ReportUtils.getReport(ancestor?.report?.parentReportID) as OnyxTypes.Report)} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 34ba0fdb9a8d..59f0a6fb322e 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1462,7 +1462,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android theme.transparent, opacity: 1, - ...(isDisabled ? styles.cursorDisabled : (isClickable ? styles.cursorPointer : styles.cursorInitial)), + ...(isDisabled ? styles.cursorDisabled : isClickable ? styles.cursorPointer : styles.cursorInitial), }), /** From 92fd58acbca1d92dbf9399c2eabce84b2a1b82c7 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 1 Apr 2024 02:55:49 +0100 Subject: [PATCH 024/617] fix lint error --- src/styles/utils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 59f0a6fb322e..1ef2298392ae 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1462,6 +1462,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android theme.transparent, opacity: 1, + // eslint-disable-next-line no-nested-ternary ...(isDisabled ? styles.cursorDisabled : isClickable ? styles.cursorPointer : styles.cursorInitial), }), From f38039d720ce9c639eac7a784d923e4501472d81 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 2 Apr 2024 04:36:58 +0100 Subject: [PATCH 025/617] added explanation comment --- src/components/ParentNavigationSubtitle.tsx | 1 + src/pages/home/report/ThreadDivider.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 24b2ddf8a10c..e8cb2e634045 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -25,6 +25,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID const {translate} = useLocalize(); + // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) if (!reportName) { return; } diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index 4500af526428..d055ea6a9da4 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -16,7 +16,8 @@ import ROUTES from '@src/ROUTES'; type ThreadDividerProps = { /** Thread ancestor */ ancestor: Ancestor; - /** Whether the link is disbled */ + + /** Whether the link is disabled */ isDisabled: boolean; }; From 928b10f8484ab5fdfd8b8680bcd6e46fc40f01b1 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:03:40 +0100 Subject: [PATCH 026/617] make the thread link look gray when it's disabled --- src/pages/home/report/ThreadDivider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index d055ea6a9da4..98bcec50750e 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -37,11 +37,11 @@ function ThreadDivider({ancestor, isDisabled}: ThreadDividerProps) { > - {translate('threads.thread')} + {translate('threads.thread')} {!ancestor.shouldDisplayNewMarker && } From 2c4a34a8e7765dbae30e0fd9ce223e7ffef4135b Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:04:58 +0100 Subject: [PATCH 027/617] prevent Thread and Replies words from being copied --- src/pages/home/report/RepliesDivider.tsx | 2 +- src/pages/home/report/ThreadDivider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/RepliesDivider.tsx b/src/pages/home/report/RepliesDivider.tsx index deac38596c99..9d237e20b9c9 100644 --- a/src/pages/home/report/RepliesDivider.tsx +++ b/src/pages/home/report/RepliesDivider.tsx @@ -19,7 +19,7 @@ function RepliesDivider({shouldHideThreadDividerLine}: RepliesDividerProps) { const {translate} = useLocalize(); return ( - + + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? ''))} accessibilityLabel={translate('threads.thread')} From 4860d792d5ee863fef8e8ca053261eec7049dfdf Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:34:48 +0100 Subject: [PATCH 028/617] prevent Thread and Replies words from being selected and copied --- src/pages/home/report/RepliesDivider.tsx | 8 ++++++-- src/pages/home/report/ThreadDivider.tsx | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/RepliesDivider.tsx b/src/pages/home/report/RepliesDivider.tsx index 9d237e20b9c9..d6d5e1d3cfc8 100644 --- a/src/pages/home/report/RepliesDivider.tsx +++ b/src/pages/home/report/RepliesDivider.tsx @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; type RepliesDividerProps = { /** Whether we should hide thread divider line */ @@ -19,14 +20,17 @@ function RepliesDivider({shouldHideThreadDividerLine}: RepliesDividerProps) { const {translate} = useLocalize(); return ( - + - {translate('threads.replies')} + {translate('threads.replies')} {!shouldHideThreadDividerLine && } ); diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index 360408c4a823..2ac2d6449f67 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -27,7 +27,10 @@ function ThreadDivider({ancestor, isDisabled}: ThreadDividerProps) { const {translate} = useLocalize(); return ( - + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? ''))} accessibilityLabel={translate('threads.thread')} From eecb6947137154352ce9867a6f80b3830aa4c397 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:14:04 +0100 Subject: [PATCH 029/617] make isDisabled prop optional --- src/pages/home/report/ThreadDivider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index 2ac2d6449f67..a6830eea3de2 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -18,10 +18,10 @@ type ThreadDividerProps = { ancestor: Ancestor; /** Whether the link is disabled */ - isDisabled: boolean; + isDisabled?: boolean; }; -function ThreadDivider({ancestor, isDisabled}: ThreadDividerProps) { +function ThreadDivider({ancestor, isDisabled = false}: ThreadDividerProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); From 9321b99225a23576a0501e8106647d941ad06009 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 4 Apr 2024 00:33:07 +0700 Subject: [PATCH 030/617] 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 bc4b3654f192fa4e94211cf4807c2c27c8b7a3d1 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:43:16 +0100 Subject: [PATCH 031/617] Remove not allowed cursor on hover for the top-most message when the user don't have access to the parent thread --- src/pages/home/report/ReportActionItem.tsx | 6 ------ src/pages/home/report/ReportActionItemParentAction.tsx | 7 +++++-- src/styles/utils/index.ts | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index a0acfe686c16..7c7ddd5e9efd 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -145,9 +145,6 @@ type ReportActionItemProps = { /** Callback to be called on onPress */ onPress?: () => void; - - /** Should press be disabled */ - isDisabled?: boolean; } & ReportActionItemOnyxProps; const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU => @@ -172,7 +169,6 @@ function ReportActionItem({ policy, transaction, onPress = undefined, - isDisabled = false, }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -835,7 +831,6 @@ function ReportActionItem({ isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} @@ -869,7 +864,6 @@ function ReportActionItem({ style={StyleUtils.getReportActionItemStyle( hovered || isWhisper || isContextMenuActive || !!isEmojiPickerActive || draftMessage !== undefined, draftMessage === undefined && !!onPress, - isDisabled, )} > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? ''))} - isDisabled={!ReportUtils.canCurrentUserOpenReport(ReportUtils.getReport(ancestor?.report?.parentReportID) as OnyxTypes.Report)} + onPress={ + ReportUtils.canCurrentUserOpenReport(ReportUtils.getReport(ancestor?.report?.parentReportID) as OnyxTypes.Report) + ? () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '')) + : undefined + } parentReportAction={parentReportAction} report={ancestor.report} reportActions={reportActions} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 1ef2298392ae..329cc4fbdf85 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1451,7 +1451,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ /** * Generate the styles for the ReportActionItem wrapper view. */ - getReportActionItemStyle: (isHovered = false, isClickable = false, isDisabled = false): ViewStyle => + getReportActionItemStyle: (isHovered = false, isClickable = false): ViewStyle => // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) // eslint-disable-next-line @typescript-eslint/no-unsafe-return ({ @@ -1463,7 +1463,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ theme.transparent, opacity: 1, // eslint-disable-next-line no-nested-ternary - ...(isDisabled ? styles.cursorDisabled : isClickable ? styles.cursorPointer : styles.cursorInitial), + ...(isClickable ? styles.cursorPointer : styles.cursorInitial), }), /** From 623345d9680499039df73712ac5cbae177ecdd31 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:25:05 +0100 Subject: [PATCH 032/617] Remove not allowed cursor on hover for the 'Thread' link for when it is disabled (use default cursor) --- .../report/ReportActionItemParentAction.tsx | 2 +- src/pages/home/report/ThreadDivider.tsx | 45 ++++++++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index f2117cb57191..d4119e738eac 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -103,7 +103,7 @@ function ReportActionItemParentAction({ > - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? ''))} - accessibilityLabel={translate('threads.thread')} - role={CONST.ROLE.BUTTON} - style={[styles.flexRow, styles.alignItemsCenter, styles.gap1]} - disabled={isDisabled} - > - - {translate('threads.thread')} - + {isLinkDisabled ? ( + <> + + {translate('threads.thread')} + + ) : ( + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? ''))} + accessibilityLabel={translate('threads.thread')} + role={CONST.ROLE.BUTTON} + style={[styles.flexRow, styles.alignItemsCenter, styles.gap1]} + > + + {translate('threads.thread')} + + )} {!ancestor.shouldDisplayNewMarker && } ); From 0f7ecdb40060b838e11d2c906f5545d523729b1d Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:52:59 +0100 Subject: [PATCH 033/617] remove comment --- src/styles/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 329cc4fbdf85..a3357b8982a1 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1462,7 +1462,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android theme.transparent, opacity: 1, - // eslint-disable-next-line no-nested-ternary ...(isClickable ? styles.cursorPointer : styles.cursorInitial), }), From 395468dc259e680429e64971479f72a40f2431a3 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 9 Apr 2024 11:39:38 +0200 Subject: [PATCH 034/617] receipt scan ui change wip --- assets/images/receipt-scan.svg | 14 ++++++++++++++ src/components/Icon/Expensicons.ts | 2 ++ .../MoneyRequestPreviewContent.tsx | 5 +++++ src/languages/en.ts | 3 ++- src/languages/es.ts | 1 + src/pages/home/report/ReportActionsList.tsx | 2 ++ 6 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 assets/images/receipt-scan.svg diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg new file mode 100644 index 000000000000..c93986de3c9b --- /dev/null +++ b/assets/images/receipt-scan.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 1fcf0d07276c..ba00ad684473 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -120,6 +120,7 @@ import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; import QrCode from '@assets/images/qrcode.svg'; import QuestionMark from '@assets/images/question-mark-circle.svg'; +import ReceiptScan from '@assets/images/receipt-scan.svg'; import ReceiptSearch from '@assets/images/receipt-search.svg'; import Receipt from '@assets/images/receipt.svg'; import RemoveMembers from '@assets/images/remove-members.svg'; @@ -283,6 +284,7 @@ export { QrCode, QuestionMark, Receipt, + ReceiptScan, RemoveMembers, ReceiptSearch, Rotate, diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 2c6f14cec4c2..8d1b7880726b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -312,6 +312,11 @@ function MoneyRequestPreviewContent({ )} + + {true && ( + {translate('iou.receiptScanInProgress')} + )} + diff --git a/src/languages/en.ts b/src/languages/en.ts index 55a4c586716a..ad5377d8349b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -632,7 +632,8 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', - receiptScanning: 'Scan in progress…', + receiptScanning: 'Receipt scanning…', + receiptScanInProgress: 'Receipt scan in progress.', receiptMissingDetails: 'Receipt missing details', missingAmount: 'Missing amount', missingMerchant: 'Missing merchant', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5956f1457005..61c6c82bbc1c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -629,6 +629,7 @@ export default { deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo en curso…', + receiptScanInProgress: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', missingMerchant: 'Falta comerciante', diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index d1b9c420b0af..e261f1f3c38e 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -6,6 +6,7 @@ import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 're import {DeviceEventEmitter, InteractionManager} from 'react-native'; import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import InvertedFlatList from '@components/InvertedFlatList'; import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; @@ -184,6 +185,7 @@ function ReportActionsList({ const hasFooterRendered = useRef(false); const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated); const lastReadTimeRef = useRef(report.lastReadTime); + Onyx.merge('transactions_8811441407757684730', {cardID: 1, merchant: 'Google', hasEReceipt: true, status: 'Pending'}); const sortedVisibleReportActions = useMemo( () => From 2a38baacb0f59c0a4349b4118adbb9edda97fea2 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:52:15 +0100 Subject: [PATCH 035/617] Fix lint error --- src/pages/home/report/ReportActionItemParentAction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index d4119e738eac..afbeb734e2b6 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -103,11 +103,11 @@ function ReportActionItemParentAction({ > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '')) : undefined } From 2e1f4ce29cca3c94151a02cc2f58a244517c25d2 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 9 Apr 2024 22:20:26 +0200 Subject: [PATCH 036/617] scanning receipt wip --- src/components/MoneyRequestHeader.tsx | 17 +++++++++++++++++ src/components/MoneyRequestHeaderStatusBar.tsx | 3 ++- .../MoneyRequestPreviewContent.tsx | 16 ++++++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f451f5f15581..ac6cfd911a6a 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -19,9 +19,13 @@ import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import HoldBanner from './HoldBanner'; +import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import {ReceiptScan} from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; +import variables from '@styles/variables'; +import theme from '@styles/theme'; type MoneyRequestHeaderOnyxProps = { /** Session info for the currently logged in user. */ @@ -216,6 +220,19 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, ); } +function ScanningReceiptHeaderTitle() { + return ( + + + + ); +} + MoneyRequestHeader.displayName = 'MoneyRequestHeader'; const MoneyRequestHeaderWithTransaction = withOnyx>({ diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 59ef4ee0bd26..828650fa8fba 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,3 +1,4 @@ +import type {ReactElement} from 'react'; import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -5,7 +6,7 @@ import Text from './Text'; type MoneyRequestHeaderStatusBarProps = { /** Title displayed in badge */ - title: string; + title: string | ReactElement; /** Banner Description */ description: string; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 8d1b7880726b..745c13ce9d0d 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -7,6 +7,7 @@ import type {GestureResponderEvent} from 'react-native'; import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import {ReceiptScan} from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -30,6 +31,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; +import variables from '@styles/variables'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -312,11 +314,17 @@ function MoneyRequestPreviewContent({ )} - - {true && ( + {isScanning && ( + + {translate('iou.receiptScanInProgress')} - )} - + + )} From 9ee70da12544f7017ed8d70e1c04b833600edfa4 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 10 Apr 2024 08:15:15 +0200 Subject: [PATCH 037/617] finalize scanning receipt --- src/components/MoneyRequestHeader.tsx | 29 ++++++++----------- .../MoneyRequestHeaderStatusBar.tsx | 14 +++++---- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index ac6cfd911a6a..2f80cf3c6e59 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as HeaderUtils from '@libs/HeaderUtils'; @@ -10,6 +11,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -24,8 +26,6 @@ import * as Expensicons from './Icon/Expensicons'; import {ReceiptScan} from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; -import variables from '@styles/variables'; -import theme from '@styles/theme'; type MoneyRequestHeaderOnyxProps = { /** Session info for the currently logged in user. */ @@ -58,6 +58,7 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, shownHoldUseExplanation = false, policy}: MoneyRequestHeaderProps) { const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); @@ -192,8 +193,15 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, )} {isScanning && ( + } + description={translate('iou.receiptScanInProgressDescription')} shouldShowBorderBottom /> )} @@ -220,19 +228,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, ); } -function ScanningReceiptHeaderTitle() { - return ( - - - - ); -} - MoneyRequestHeader.displayName = 'MoneyRequestHeader'; const MoneyRequestHeaderWithTransaction = withOnyx>({ diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 828650fa8fba..d21e66ba39eb 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,4 +1,4 @@ -import type {ReactElement} from 'react'; +import type {ReactNode} from 'react'; import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -6,7 +6,7 @@ import Text from './Text'; type MoneyRequestHeaderStatusBarProps = { /** Title displayed in badge */ - title: string | ReactElement; + title: string | ReactNode; /** Banner Description */ description: string; @@ -20,9 +20,13 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; return ( - - {title} - + {typeof title === 'string' ? ( + + {title} + + ) : ( + {title} + )} {description} diff --git a/src/languages/en.ts b/src/languages/en.ts index ad5377d8349b..c7ea86ff8064 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -634,6 +634,7 @@ export default { routePending: 'Route pending...', receiptScanning: 'Receipt scanning…', receiptScanInProgress: 'Receipt scan in progress.', + receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', receiptMissingDetails: 'Receipt missing details', missingAmount: 'Missing amount', missingMerchant: 'Missing merchant', diff --git a/src/languages/es.ts b/src/languages/es.ts index 61c6c82bbc1c..c9aabe62087a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -630,6 +630,7 @@ export default { routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo en curso…', receiptScanInProgress: 'Escaneo en curso…', + receiptScanInProgressDescription: 'Escaneo en curso.', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', missingMerchant: 'Falta comerciante', From 8f22291f1a8ea5b6b2953cf13e7a17202502f6ce Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 10 Apr 2024 17:18:46 +0200 Subject: [PATCH 038/617] finalize pending transaction, cleanup wip --- assets/images/credit-card-hourglass.svg | 19 +++++++++++++++ src/components/Icon/Expensicons.ts | 2 ++ src/components/MoneyRequestHeader.tsx | 14 +++++++---- .../MoneyRequestPreviewContent.tsx | 10 -------- .../ReportActionItem/MoneyRequestView.tsx | 5 ---- .../ReportActionItem/ReportPreview.tsx | 14 +++++++++++ src/languages/en.ts | 3 ++- src/libs/CardUtils.ts | 2 +- src/pages/home/ReportScreen.tsx | 23 ++++++++++--------- src/pages/home/report/ReportActionItem.tsx | 1 + src/pages/home/report/ReportActionsList.tsx | 2 +- 11 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 assets/images/credit-card-hourglass.svg diff --git a/assets/images/credit-card-hourglass.svg b/assets/images/credit-card-hourglass.svg new file mode 100644 index 000000000000..2acd013fbe59 --- /dev/null +++ b/assets/images/credit-card-hourglass.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index ba00ad684473..78583f3af4d4 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -41,6 +41,7 @@ import Collapse from '@assets/images/collapse.svg'; import Concierge from '@assets/images/concierge.svg'; import Connect from '@assets/images/connect.svg'; import Copy from '@assets/images/copy.svg'; +import CreditCardHourglass from '@assets/images/credit-card-hourglass.svg'; import CreditCard from '@assets/images/creditcard.svg'; import DocumentPlus from '@assets/images/document-plus.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; @@ -198,6 +199,7 @@ export { Connect, Copy, CreditCard, + CreditCardHourglass, DeletedRoomAvatar, Document, DocumentSlash, diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 2f80cf3c6e59..5ea46f339d46 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -23,7 +23,6 @@ import HeaderWithBackButton from './HeaderWithBackButton'; import HoldBanner from './HoldBanner'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; -import {ReceiptScan} from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; @@ -186,8 +185,15 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, /> {isPending && ( + } + description={translate('iou.transactionPendingDescription')} shouldShowBorderBottom={!isScanning} /> )} @@ -195,7 +201,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, 0; const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)) || ReportUtils.hasActionsWithErrors(iouReportID); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); @@ -226,6 +228,7 @@ function ReportPreview({ const shouldShowSingleRequestMerchantOrDescription = numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1); + const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && transactionsWithReceipts.length === 1; const {isSupportTextHtml, supportText} = useMemo(() => { if (formattedMerchant) { @@ -318,6 +321,17 @@ function ReportPreview({ )} + {shouldShowPendingSubtitle && ( + + + {translate('iou.transactionPending')} + + )} {shouldShowSettlementButton && ( diff --git a/src/languages/en.ts b/src/languages/en.ts index c7ea86ff8064..27637aeb9602 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -641,7 +641,7 @@ export default { receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", receiptScanningFailed: 'Receipt scanning failed. Enter the details manually.', - transactionPendingText: 'It takes a few days from the date the card was used for the transaction to post.', + transactionPendingDescription: 'Transaction pending. It can take a few days from the date the card was used for the transaction to post.', requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('request', 'requests', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' @@ -737,6 +737,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', + transactionPending: 'Transaction pending.', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index c4d67adcd54a..0d8de86f63bc 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -46,7 +46,7 @@ function isExpensifyCard(cardID?: number) { * @returns boolean if the cardID is in the cardList from ONYX. Includes Expensify Cards. */ function isCorporateCard(cardID: number) { - return !!allCards[cardID]; + return true; } /** diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 940cba181db7..e8c00f807e06 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -323,7 +323,7 @@ function ReportScreen({ /> ); - if (isSingleTransactionView) { + if (true) { headerView = ( ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? []), [reportActions]); - if (ReportUtils.isMoneyRequestReport(report)) { - headerView = ( - - ); - } + // if (ReportUtils.isMoneyRequestReport(report)) { + // headerView = ( + // + // ); + // } /** * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. @@ -603,6 +603,7 @@ function ReportScreen({ ); } + console.log('REPORT SCREEN'); return ( diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index eeeb5b95273c..02d139780ed0 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -832,6 +832,7 @@ function ReportActionItem({ ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; + return ( @@ -196,6 +195,7 @@ function ReportActionsList({ ), [sortedReportActions, isOffline], ); + const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); const hasNewestReportAction = sortedReportActions?.[0].created === report.lastVisibleActionCreated; From efe88ce911f70c4d74fe90d08c42cb17c7b9b0c9 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 10 Apr 2024 17:31:16 +0200 Subject: [PATCH 039/617] cleanup --- src/languages/es.ts | 5 +++-- src/pages/home/report/ReportActionsList.tsx | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index c9aabe62087a..a6a19d2bf35c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -630,14 +630,14 @@ export default { routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo en curso…', receiptScanInProgress: 'Escaneo en curso…', - receiptScanInProgressDescription: 'Escaneo en curso.', + receiptScanInProgressDescription: ' Escaneando recibo. Vuelva a comprobarlo más tarde o introduzca los detalles ahora.', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', missingMerchant: 'Falta comerciante', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.', - transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', + transactionPendingDescription: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' @@ -735,6 +735,7 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', + transactionPending: 'Transaction pending.', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 0068ed875b82..60a620f186cc 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -6,7 +6,6 @@ import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 're import {DeviceEventEmitter, InteractionManager} from 'react-native'; import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import InvertedFlatList from '@components/InvertedFlatList'; import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; From 9a664e5d4db4201533fe1d9c9180b6fe4bdf81a9 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 10 Apr 2024 17:43:25 +0200 Subject: [PATCH 040/617] cleanup --- src/pages/home/ReportScreen.tsx | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e8c00f807e06..940cba181db7 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -323,7 +323,7 @@ function ReportScreen({ /> ); - if (true) { + if (isSingleTransactionView) { headerView = ( ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? []), [reportActions]); - // if (ReportUtils.isMoneyRequestReport(report)) { - // headerView = ( - // - // ); - // } + if (ReportUtils.isMoneyRequestReport(report)) { + headerView = ( + + ); + } /** * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. @@ -603,7 +603,6 @@ function ReportScreen({ ); } - console.log('REPORT SCREEN'); return ( From d315efb2d08469d717e820189c2d4a388976cfee Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 10 Apr 2024 17:57:36 +0200 Subject: [PATCH 041/617] cleanup --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 0d8de86f63bc..c4d67adcd54a 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -46,7 +46,7 @@ function isExpensifyCard(cardID?: number) { * @returns boolean if the cardID is in the cardList from ONYX. Includes Expensify Cards. */ function isCorporateCard(cardID: number) { - return true; + return !!allCards[cardID]; } /** From 4e08e070c709b6e8350d3cbe05221a8b56098ac0 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 11 Apr 2024 00:26:01 +0200 Subject: [PATCH 042/617] cleanup translations --- src/languages/es.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index a6a19d2bf35c..877d745fa59c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -628,16 +628,16 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', - receiptScanning: 'Escaneo en curso…', - receiptScanInProgress: 'Escaneo en curso…', - receiptScanInProgressDescription: ' Escaneando recibo. Vuelva a comprobarlo más tarde o introduzca los detalles ahora.', + receiptScanning: 'Escaneando recibo…', + receiptScanInProgress: 'Escaneo en curso.', + receiptScanInProgressDescription: 'Escaneando recibo. Vuelva a comprobarlo más tarde o introduzca los detalles ahora.', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', missingMerchant: 'Falta comerciante', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.', - transactionPendingDescription: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', + transactionPendingDescription: 'Transacción pendiente. Esto puede tardar algunos días en registrarse a partir de la fecha en que se utilizó la tarjeta.', requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' @@ -735,7 +735,7 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', - transactionPending: 'Transaction pending.', + transactionPending: 'Transacción pendiente.', }, notificationPreferencesPage: { header: 'Preferencias de avisos', From 648e333f6178eb2b4699d024936d4ef281aacd7d Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 11 Apr 2024 00:41:33 +0200 Subject: [PATCH 043/617] fix preview --- .../MoneyRequestPreviewContent.tsx | 12 ++++++++++++ src/pages/home/report/ReportActionsList.tsx | 1 + 2 files changed, 13 insertions(+) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index b0363135e273..e149891b0365 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -87,6 +87,7 @@ function MoneyRequestPreviewContent({ const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const isPending = TransactionUtils.isPending(transaction); const isOnHold = TransactionUtils.isOnHold(transaction); const isSettlementOrApprovalPartial = Boolean(iouReport?.pendingFields?.partial); const isPartialHold = isSettlementOrApprovalPartial && isOnHold; @@ -315,6 +316,17 @@ function MoneyRequestPreviewContent({ {translate('iou.receiptScanInProgress')} )} + {isPending && ( + + + {translate('iou.transactionPending')} + + )} diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 60a620f186cc..0068ed875b82 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -6,6 +6,7 @@ import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 're import {DeviceEventEmitter, InteractionManager} from 'react-native'; import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import InvertedFlatList from '@components/InvertedFlatList'; import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; From f2bcf10e8e12bef7242a95adbab46f0a3352e04c Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 11 Apr 2024 15:57:59 +0200 Subject: [PATCH 044/617] wip --- ios/Podfile.lock | 2 +- src/components/MoneyRequestHeader.tsx | 2 ++ .../MoneyRequestPreviewContent.tsx | 1 + .../ReportActionItem/ReportPreview.tsx | 14 +++++++++++++- src/libs/ReportUtils.ts | 1 + src/pages/home/ReportScreen.tsx | 2 ++ src/pages/home/report/ReportActionsList.tsx | 19 +++++++++++++++++++ 7 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 32a8bca75bcd..94bd6e35f31d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1921,7 +1921,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 9f26224fce1233ffdad9fa4e56863e3de2190dc0 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5ea46f339d46..909b6e6f71f6 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -165,6 +165,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, }); } + console.log('MONEY REQUEST HEADER'); + return ( <> diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e149891b0365..2a56e2f2405d 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -67,6 +67,7 @@ function MoneyRequestPreviewContent({ const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const parser = new ExpensiMark(); + console.warn('TRANSACTION ', transaction); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? -1; const ownerAccountID = iouReport?.ownerAccountID ?? -1; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 40c9f1afcc21..5b9c2269028c 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -228,7 +228,8 @@ function ReportPreview({ const shouldShowSingleRequestMerchantOrDescription = numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1); - const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && transactionsWithReceipts.length === 1; + const shouldShowScanningSubtitle = numberOfScanningReceipts === 1 && allTransactions.length === 1; + const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && allTransactions.length === 1; const {isSupportTextHtml, supportText} = useMemo(() => { if (formattedMerchant) { @@ -321,6 +322,17 @@ function ReportPreview({ )} + {shouldShowScanningSubtitle && ( + + + {translate('iou.receiptScanInProgress')} + + )} {shouldShowPendingSubtitle && ( ): boolean { */ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + console.log('REPORT ', report); return isIOURequest(report) || isExpenseRequest(report); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 940cba181db7..de02342ff414 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -323,6 +323,8 @@ function ReportScreen({ /> ); + console.warn('ReportUtils.isMoneyRequest(report) ', ReportUtils.isMoneyRequest(report)); + if (isSingleTransactionView) { headerView = ( { const unsubscriber = Visibility.onVisibilityChange(() => { setIsVisible(Visibility.isVisible()); From c3285b8e4baf6fc2cf64dd0cc17dd3c3fb10c6ec Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 11 Apr 2024 16:18:56 +0200 Subject: [PATCH 045/617] cleanup --- src/components/MoneyRequestHeader.tsx | 2 -- .../MoneyRequestPreviewContent.tsx | 1 - src/libs/ReportUtils.ts | 1 - src/pages/home/ReportScreen.tsx | 2 -- src/pages/home/report/ReportActionsList.tsx | 20 ------------------- 5 files changed, 26 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 909b6e6f71f6..5ea46f339d46 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -165,8 +165,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, }); } - console.log('MONEY REQUEST HEADER'); - return ( <> diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 2a56e2f2405d..e149891b0365 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -67,7 +67,6 @@ function MoneyRequestPreviewContent({ const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const parser = new ExpensiMark(); - console.warn('TRANSACTION ', transaction); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? -1; const ownerAccountID = iouReport?.ownerAccountID ?? -1; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f03a5051d634..fec64efaac7f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1282,7 +1282,6 @@ function isTrackExpenseReport(report: OnyxEntry): boolean { */ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; - console.log('REPORT ', report); return isIOURequest(report) || isExpenseRequest(report); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index de02342ff414..940cba181db7 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -323,8 +323,6 @@ function ReportScreen({ /> ); - console.warn('ReportUtils.isMoneyRequest(report) ', ReportUtils.isMoneyRequest(report)); - if (isSingleTransactionView) { headerView = ( { const unsubscriber = Visibility.onVisibilityChange(() => { setIsVisible(Visibility.isVisible()); From fea7ada105b4d1cbc82e9ff5ebf723e9b40c07b9 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 15 Apr 2024 11:56:37 +0200 Subject: [PATCH 046/617] fix typography --- src/components/MoneyRequestHeader.tsx | 8 ++++---- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 8 ++++++-- src/components/ReportActionItem/ReportPreview.tsx | 8 ++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5ea46f339d46..7557799472e1 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -188,8 +188,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, title={ } @@ -202,8 +202,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, title={ } diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 08d693dd109a..63950e3fe8d0 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -313,7 +313,9 @@ function MoneyRequestPreviewContent({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - {translate('iou.receiptScanInProgress')} + + {translate('iou.receiptScanInProgress')} + )} {isPending && ( @@ -324,7 +326,9 @@ function MoneyRequestPreviewContent({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - {translate('iou.transactionPending')} + + {translate('iou.transactionPending')} + )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 231b1176423b..2b5034708dac 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -330,7 +330,9 @@ function ReportPreview({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - {translate('iou.receiptScanInProgress')} + + {translate('iou.receiptScanInProgress')} + )} {shouldShowPendingSubtitle && ( @@ -341,7 +343,9 @@ function ReportPreview({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - {translate('iou.transactionPending')} + + {translate('iou.transactionPending')} + )} From dd4ae04abcefd6e6768ca036bbf32744a02c493c Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 15 Apr 2024 17:16:42 +0200 Subject: [PATCH 047/617] style fixes --- src/components/MoneyRequestHeader.tsx | 4 ++-- src/components/MoneyRequestHeaderStatusBar.tsx | 2 +- .../MoneyRequestPreviewContent.tsx | 8 ++------ src/components/ReportActionItem/ReportPreview.tsx | 12 ++++-------- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 7557799472e1..c50913bc2d31 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -190,7 +190,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, src={Expensicons.CreditCardHourglass} height={variables.iconSizeSmall} width={variables.iconSizeSmall} - fill={theme.textSupporting} + fill={theme.icon} /> } description={translate('iou.transactionPendingDescription')} @@ -204,7 +204,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, src={Expensicons.ReceiptScan} height={variables.iconSizeSmall} width={variables.iconSizeSmall} - fill={theme.textSupporting} + fill={theme.icon} /> } description={translate('iou.receiptScanInProgressDescription')} diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index d21e66ba39eb..0052768a4cf0 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -25,7 +25,7 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom {title} ) : ( - {title} + {title} )} {description} diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 63950e3fe8d0..c234eb749653 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -313,9 +313,7 @@ function MoneyRequestPreviewContent({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - - {translate('iou.receiptScanInProgress')} - + {translate('iou.receiptScanInProgress')} )} {isPending && ( @@ -326,9 +324,7 @@ function MoneyRequestPreviewContent({ width={variables.iconSizeExtraSmall} fill={theme.textSupporting} /> - - {translate('iou.transactionPending')} - + {translate('iou.transactionPending')} )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 2b5034708dac..766680fd378b 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -328,11 +328,9 @@ function ReportPreview({ src={Expensicons.ReceiptScan} height={variables.iconSizeExtraSmall} width={variables.iconSizeExtraSmall} - fill={theme.textSupporting} + fill={theme.icon} /> - - {translate('iou.receiptScanInProgress')} - + {translate('iou.receiptScanInProgress')} )} {shouldShowPendingSubtitle && ( @@ -341,11 +339,9 @@ function ReportPreview({ src={Expensicons.CreditCardHourglass} height={variables.iconSizeExtraSmall} width={variables.iconSizeExtraSmall} - fill={theme.textSupporting} + fill={theme.icon} /> - - {translate('iou.transactionPending')} - + {translate('iou.transactionPending')} )} From d99170a61a4d44c2b750123bf14d374b82869214 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 00:35:57 +0100 Subject: [PATCH 048/617] Add TS definitions --- src/types/onyx/Transaction.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 281b6b4228ce..46b60ee8b6ec 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -98,6 +98,13 @@ type TaxRate = { data?: TaxRateData; }; +type SplitShare = { + amount: number; + isModified?: boolean; +}; + +type SplitShares = Record; + type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< { /** The original transaction amount */ @@ -215,6 +222,12 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** Indicates transaction loading */ isLoading?: boolean; + + /** Holds individual shares of a split keyed by accountID, only used locally */ + splitShares?: SplitShares; + + /** Holds the accountIDs of accounts who paid the split, for now only supports a single payer */ + splitPayerAccountIDs?: number[]; }, keyof Comment >; @@ -245,4 +258,6 @@ export type { TaxRate, ReceiptSource, TransactionCollectionDataSet, + SplitShare, + SplitShares, }; From 99efbb6a82e1955db820d69106ddf7c311d5bad9 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 00:47:20 +0100 Subject: [PATCH 049/617] Set split shares when first creating the split --- src/libs/actions/IOU.ts | 22 ++++++++++++++++++- .../iou/request/step/IOURequestStepAmount.tsx | 4 ++++ .../step/IOURequestStepParticipants.tsx | 8 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cd0264ddb6ea..70f611ac65e9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -56,7 +56,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {Comment, Receipt, ReceiptSource, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; +import type {Comment, Receipt, ReceiptSource, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; @@ -5388,6 +5388,25 @@ function setShownHoldUseExplanation() { Onyx.set(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, true); } +function resetSplitShares(transactionID: string, participantAccountIDs: number[], amount: number, currency: string) { + const participantAccountIDsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== userAccountID); + const splitShares: SplitShares = [userAccountID, ...participantAccountIDsWithoutCurrentUser].reduce((result: SplitShares, accountID): SplitShares => { + const isPayer = accountID === userAccountID; + const splitAmount = IOUUtils.calculateAmount(participantAccountIDsWithoutCurrentUser.length, amount, currency, isPayer); + return { + ...result, + [accountID]: { + amount: splitAmount, + }, + }; + }, {}); + + // TODO: figure out why this needs `then` + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares: null}).then(() => { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares}); + }); +} + /** * Put money request on HOLD */ @@ -5582,6 +5601,7 @@ export { setMoneyRequestTaxAmount, setMoneyRequestTaxRate, setShownHoldUseExplanation, + resetSplitShares, updateMoneyRequestDate, updateMoneyRequestBillable, updateMoneyRequestMerchant, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 1936a132c665..f722c9e2b17e 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -127,6 +127,10 @@ function IOURequestStepAmount({ // to the confirm step. if (report?.reportID) { IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + if (isSplitBill && !report.isOwnPolicyExpenseChat && report.participants) { + const participantAccountIDs = Object.keys(report.participants).map((accountID) => Number(accountID)); + IOU.resetSplitShares(transactionID, participantAccountIDs, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD); + } Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index cebb000b2121..320cfb97e8f1 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -133,11 +133,17 @@ function IOURequestStepParticipants({ nextStepIOUType = CONST.IOU.TYPE.SEND; } + const isPolicyExpenseChat = participants?.some((participant) => participant.isPolicyExpenseChat); + if (nextStepIOUType === CONST.IOU.TYPE.SPLIT && !isPolicyExpenseChat) { + const participantAccountIDs = participants?.map((participant) => participant.accountID) as number[]; + IOU.resetSplitShares(transactionID, participantAccountIDs, transaction?.amount ?? 0, transaction?.currency ?? CONST.CURRENCY.USD); + } + IOU.setMoneyRequestTag(transactionID, ''); IOU.setMoneyRequestCategory(transactionID, ''); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, nextStepIOUType, transactionID, selectedReportID.current || reportID)); }, - [iouType, transactionID, reportID], + [iouType, transactionID, reportID, participants, transaction?.amount, transaction?.currency], ); const navigateBack = useCallback(() => { From cf6504400af7bf79d8464faabf42e99072db5b22 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 03:01:04 +0100 Subject: [PATCH 050/617] Add IOU functions to set split shares and adjust them --- src/libs/actions/IOU.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 70f611ac65e9..2b7d08b1346a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5407,6 +5407,27 @@ function resetSplitShares(transactionID: string, participantAccountIDs: number[] }); } +function setSplitShare(transactionID: string, participantAccountID: number, participantShare: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + splitShares: { + [participantAccountID]: {amount: participantShare, isModified: true}, + }, + }); +} + +function adjustRemainingSplitShares(transactionID: string, remainingAccountIDs: number[], remainingAmount: number, currency: string) { + const splitShares: SplitShares = remainingAccountIDs.reduce((result: SplitShares, accountID: number, index: number): SplitShares => { + const splitAmount = IOUUtils.calculateAmount(remainingAccountIDs.length - 1, remainingAmount, currency, index === 0); + return { + ...result, + [accountID]: { + amount: splitAmount, + }, + }; + }, {}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares}); +} + /** * Put money request on HOLD */ @@ -5602,6 +5623,8 @@ export { setMoneyRequestTaxRate, setShownHoldUseExplanation, resetSplitShares, + setSplitShare, + adjustRemainingSplitShares, updateMoneyRequestDate, updateMoneyRequestBillable, updateMoneyRequestMerchant, From a743ffcb12f76928915760d1524fc9eb036489b6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Apr 2024 15:10:05 +0700 Subject: [PATCH 051/617] fix empty chat displayed in focus mode --- src/libs/ReportUtils.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f31b4a780c5a..5b3f0e66ffd8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4470,11 +4470,23 @@ function buildOptimisticMoneyRequestEntities( return [createdActionForChat, createdActionForIOUReport, iouAction, transactionThread, createdActionForTransactionThread]; } +// Check if the report is empty report +function isEmptyReport(report: OnyxEntry): boolean { + if (!report) { + return true; + } + const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); + return !report.lastMessageText && !report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey; +} + function isUnread(report: OnyxEntry): boolean { if (!report) { return false; } + if (isEmptyReport(report)) { + return false; + } // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; const lastReadTime = report.lastReadTime ?? ''; @@ -4678,8 +4690,8 @@ function shouldReportBeInOptionList({ if (hasDraftComment || requiresAttentionFromCurrentUser(report)) { return true; } - const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); - const isEmptyChat = !report.lastMessageText && !report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey; + + const isEmptyChat = isEmptyReport(report); const canHideReport = shouldHideReport(report, currentReportId); // Include reports if they are pinned From 787ad3b011378172c0bc70d454d962a6a8676e36 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Apr 2024 15:58:57 +0700 Subject: [PATCH 052/617] fix jest test --- tests/unit/SidebarOrderTest.ts | 16 +++++++----- tests/unit/SidebarTest.ts | 2 ++ tests/unit/UnreadIndicatorUpdaterTest.ts | 32 ++++++++++++++++++++---- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 2758d43fb81e..0b8ec5b1385f 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -861,10 +861,10 @@ describe('Sidebar', () => { it('alphabetizes chats', () => { LHNTestUtils.getDefaultRenderedSidebarLinks(); - const report1 = LHNTestUtils.getFakeReport([1, 2], 3, true); - const report2 = LHNTestUtils.getFakeReport([3, 4], 2, true); - const report3 = LHNTestUtils.getFakeReport([5, 6], 1, true); - const report4 = LHNTestUtils.getFakeReport([7, 8], 0, true); + const report1 = {...LHNTestUtils.getFakeReport([1, 2], 3, true), lastMessageText: 'test'}; + const report2 = {...LHNTestUtils.getFakeReport([3, 4], 2, true), lastMessageText: 'test'}; + const report3 = {...LHNTestUtils.getFakeReport([5, 6], 1, true), lastMessageText: 'test'}; + const report4 = {...LHNTestUtils.getFakeReport([7, 8], 0, true), lastMessageText: 'test'}; const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -918,9 +918,13 @@ describe('Sidebar', () => { chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, + lastMessageText: 'test', }; - const report2 = LHNTestUtils.getFakeReport([3, 4], 2, true); - const report3 = LHNTestUtils.getFakeReport([5, 6], 1, true); + const report2 = { + ...LHNTestUtils.getFakeReport([3, 4], 2, true), + lastMessageText: 'test', + }; + const report3 = {...LHNTestUtils.getFakeReport([5, 6], 1, true), lastMessageText: 'test'}; // Given the user is in all betas const betas = [CONST.BETAS.DEFAULT_ROOMS]; diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index 23ea0d377634..75f8fa256c57 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -42,6 +42,7 @@ describe('Sidebar', () => { chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, + lastMessageText: 'test', }; const action = { @@ -94,6 +95,7 @@ describe('Sidebar', () => { chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, + lastMessageText: 'test', }; const action = { ...LHNTestUtils.getFakeReportAction('email1@test.com', 3), diff --git a/tests/unit/UnreadIndicatorUpdaterTest.ts b/tests/unit/UnreadIndicatorUpdaterTest.ts index a5f58b57793a..22141eee791d 100644 --- a/tests/unit/UnreadIndicatorUpdaterTest.ts +++ b/tests/unit/UnreadIndicatorUpdaterTest.ts @@ -6,9 +6,23 @@ describe('UnreadIndicatorUpdaterTest', () => { describe('should return correct number of unread reports', () => { it('given last read time < last visible action created', () => { const reportsToBeUsed = { - 1: {reportID: '1', reportName: 'test', type: CONST.REPORT.TYPE.EXPENSE, lastReadTime: '2023-07-08 07:15:44.030', lastVisibleActionCreated: '2023-08-08 07:15:44.030'}, - 2: {reportID: '2', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastReadTime: '2023-02-05 09:12:05.000', lastVisibleActionCreated: '2023-02-06 07:15:44.030'}, - 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK}, + 1: { + reportID: '1', + reportName: 'test', + type: CONST.REPORT.TYPE.EXPENSE, + lastReadTime: '2023-07-08 07:15:44.030', + lastVisibleActionCreated: '2023-08-08 07:15:44.030', + lastMessageText: 'test', + }, + 2: { + reportID: '2', + reportName: 'test', + type: CONST.REPORT.TYPE.TASK, + lastReadTime: '2023-02-05 09:12:05.000', + lastVisibleActionCreated: '2023-02-06 07:15:44.030', + lastMessageText: 'test', + }, + 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; expect(getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(2); }); @@ -31,9 +45,17 @@ describe('UnreadIndicatorUpdaterTest', () => { notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, lastReadTime: '2023-07-08 07:15:44.030', lastVisibleActionCreated: '2023-08-08 07:15:44.030', + lastMessageText: 'test', + }, + 2: { + reportID: '2', + reportName: 'test', + type: CONST.REPORT.TYPE.TASK, + lastReadTime: '2023-02-05 09:12:05.000', + lastVisibleActionCreated: '2023-02-06 07:15:44.030', + lastMessageText: 'test', }, - 2: {reportID: '2', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastReadTime: '2023-02-05 09:12:05.000', lastVisibleActionCreated: '2023-02-06 07:15:44.030'}, - 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK}, + 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; expect(getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(1); }); From 97ac99107789143be0118508ac75ec12bf09ae80 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 17 Apr 2024 23:09:41 +0100 Subject: [PATCH 053/617] Allow configuring split amounts --- ...raryForRefactorRequestConfirmationList.tsx | 83 +++++-------------- 1 file changed, 20 insertions(+), 63 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 21815f00253b..215c6f3b0ea9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -190,7 +190,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: pickedParticipants, payeePersonalDetails, - canModifyParticipants = false, session, isReadOnly = false, bankAccountRoute = '', @@ -228,6 +227,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + console.log(transaction); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -339,17 +339,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true); }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); - }, - [iouAmount, iouCurrencyCode], - ); - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); @@ -380,45 +369,26 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants); - useEffect(() => { - userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants; - }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]); - const shouldDisablePaidBySection = userCanModifyParticipants.current; const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipants) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + console.log(transaction?.splitShares); + console.log(transaction?.splitShares?.[payeeOption.accountID]?.amount); + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ + ...participantOption, + descriptiveText: null, + amountValue: transaction?.splitShares?.[participantOption.accountID]?.amount, + amountCurrency: iouCurrencyCode, + onAmountChange: (value) => {}, + })); - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - personalDetailsOfPayee, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + sections.push({ + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -431,18 +401,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - pickedParticipants, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - personalDetailsOfPayee, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); + }, [transaction?.splitShares, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -957,18 +916,16 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) Date: Wed, 17 Apr 2024 23:10:49 +0100 Subject: [PATCH 054/617] Use temporary amount input --- src/components/AmountTextInput.tsx | 18 +- src/components/MoneyRequestAmountInput.tsx | 206 ++++++++++++++++++ ...raryForRefactorRequestConfirmationList.tsx | 21 +- src/components/OptionRow.tsx | 10 +- .../TextInput/BaseTextInput/index.tsx | 6 +- 5 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 src/components/MoneyRequestAmountInput.tsx diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index abdef6707327..ff20bbb79ac2 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -34,7 +34,19 @@ type AmountTextInputProps = { } & Pick; function AmountTextInput( - {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, ...rest}: AmountTextInputProps, + { + formattedAmount, + onChangeAmount, + placeholder, + selection, + onSelectionChange, + style, + touchableInputWrapperStyle = null, + inputStyle = null, + textInputContainerStyles, + onKeyPress, + ...rest + }: AmountTextInputProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -44,8 +56,8 @@ function AmountTextInput( autoGrow hideFocusedState shouldInterceptSwipe - inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} - textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} + inputStyle={inputStyle ?? [styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} + textInputContainerStyles={textInputContainerStyles ?? [styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} onChangeText={onChangeAmount} ref={ref} value={formattedAmount} diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx new file mode 100644 index 000000000000..806c31b1c499 --- /dev/null +++ b/src/components/MoneyRequestAmountInput.tsx @@ -0,0 +1,206 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import * as Browser from '@libs/Browser'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import type {MaybePhraseKey} from '@libs/Localize'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import CONST from '@src/CONST'; +import type {SelectedTabRequest} from '@src/types/onyx'; +import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; +import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; + +type MoneyRequestAmountFormProps = { + /** IOU amount saved in Onyx */ + amount?: number; + + /** Currency chosen by user or saved in Onyx */ + currency?: string; + + /** Whether the currency symbol is pressable */ + isCurrencyPressable?: boolean; + + hideCurrencySymbol?: boolean; + + prefixCharacter?: string; + + /** Fired when back button pressed, navigates to currency selection page */ + onCurrencyButtonPress?: () => void; + + /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ + selectedTab?: SelectedTabRequest; +}; + +type Selection = { + start: number; + end: number; +}; + +/** + * Returns the new selection object based on the updated amount's length + */ +const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: number): Selection => { + const cursorPosition = oldSelection.end + (newLength - prevLength); + return {start: cursorPosition, end: cursorPosition}; +}; + +function MoneyRequestAmountTextInput( + { + amount = 0, + currency = CONST.CURRENCY.USD, + isCurrencyPressable = true, + onCurrencyButtonPress, + prefixCharacter, + hideCurrencySymbol, + inputStyle = null, + textInputContainerStyles = null, + selectedTab = CONST.TAB_REQUEST.MANUAL, + }: MoneyRequestAmountFormProps, + forwardedRef: ForwardedRef, +) { + const {toLocaleDigit, numberFormat} = useLocalize(); + const textInput = useRef(null); + + const decimals = CurrencyUtils.getCurrencyDecimals(currency); + const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; + + const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); + const [formError, setFormError] = useState(''); + + const [selection, setSelection] = useState({ + start: selectedAmountAsString.length, + end: selectedAmountAsString.length, + }); + + const forwardDeletePressedRef = useRef(false); + + const initializeAmount = useCallback((newAmount: number) => { + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : ''; + setCurrentAmount(frontendAmount); + setSelection({ + start: frontendAmount.length, + end: frontendAmount.length, + }); + }, []); + + useEffect(() => { + if (!currency || typeof amount !== 'number') { + return; + } + initializeAmount(amount); + // we want to re-initialize the state only when the amount changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amount]); + + /** + * Sets the selection and the amount accordingly to the value passed to the input + * @param {String} newAmount - Changed amount from user input + */ + const setNewAmount = useCallback( + (newAmount: string) => { + // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value + // More info: https://github.com/Expensify/App/issues/16974 + const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount); + // Use a shallow copy of selection to trigger setSelection + // More info: https://github.com/Expensify/App/issues/16385 + if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals)) { + setSelection((prevSelection) => ({...prevSelection})); + return; + } + if (formError) { + setFormError(''); + } + + // setCurrentAmount contains another setState(setSelection) making it error-prone since it is leading to setSelection being called twice for a single setCurrentAmount call. This solution introducing the hasSelectionBeenSet flag was chosen for its simplicity and lower risk of future errors https://github.com/Expensify/App/issues/23300#issuecomment-1766314724. + + let hasSelectionBeenSet = false; + setCurrentAmount((prevAmount) => { + const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); + const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current; + if (!hasSelectionBeenSet) { + hasSelectionBeenSet = true; + setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); + } + return strippedAmount; + }); + }, + [decimals, formError], + ); + + useEffect(() => {}); + + // Modifies the amount to match the decimals for changed currency. + useEffect(() => { + // If the changed currency supports decimals, we can return + if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) { + return; + } + + // If the changed currency doesn't support decimals, we can strip the decimals + setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount)); + + // we want to update only when decimals change (setNewAmount also changes when decimals change). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setNewAmount]); + + /** + * Input handler to check for a forward-delete key (or keyboard shortcut) press. + */ + const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent) => { + const key = nativeEvent?.key.toLowerCase(); + if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) { + // Optimistically anticipate forward-delete on iOS Safari (in cases where the Mac Accessiblity keyboard is being + // used for input). If the Control-D shortcut doesn't get sent, the ref will still be reset on the next key press. + forwardDeletePressedRef.current = true; + return; + } + // Control-D on Mac is a keyboard shortcut for forward-delete. See https://support.apple.com/en-us/HT201236 for Mac keyboard shortcuts. + // Also check for the keyboard shortcut on iOS in cases where a hardware keyboard may be connected to the device. + const operatingSystem = getOperatingSystem(); + forwardDeletePressedRef.current = key === 'delete' || ((operatingSystem === CONST.OS.MAC_OS || operatingSystem === CONST.OS.IOS) && nativeEvent?.ctrlKey && key === 'd'); + }; + + const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); + + useEffect(() => { + setFormError(''); + }, [selectedTab]); + + return ( + { + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } else if (forwardedRef?.current) { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = ref; + } + textInput.current = ref; + }} + selectedCurrencyCode={currency} + selection={selection} + onSelectionChange={(e: NativeSyntheticEvent) => { + const maxSelection = formattedAmount.length; + const start = Math.min(e.nativeEvent.selection.start, maxSelection); + const end = Math.min(e.nativeEvent.selection.end, maxSelection); + setSelection({start, end}); + }} + onKeyPress={textInputKeyPress} + isCurrencyPressable={isCurrencyPressable} + hideCurrencySymbol={hideCurrencySymbol} + prefixCharacter={prefixCharacter} + inputStyle={inputStyle} + textInputContainerStyles={textInputContainerStyles} + /> + ); +} + +MoneyRequestAmountTextInput.displayName = 'MoneyRequestAmountTextInput'; + +export default React.forwardRef(MoneyRequestAmountTextInput); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 215c6f3b0ea9..3f3826d14ca3 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -190,6 +190,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: pickedParticipants, payeePersonalDetails, + currencyList, session, isReadOnly = false, bankAccountRoute = '', @@ -227,7 +228,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - console.log(transaction); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -374,14 +374,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - console.log(transaction?.splitShares); - console.log(transaction?.splitShares?.[payeeOption.accountID]?.amount); const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ ...participantOption, descriptiveText: null, - amountValue: transaction?.splitShares?.[participantOption.accountID]?.amount, - amountCurrency: iouCurrencyCode, - onAmountChange: (value) => {}, + amountProps: { + value: transaction?.splitShares?.[participantOption.accountID]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, + isCurrencyPressable: false, + hideCurrencySymbol: true, + style: [{minWidth: 100}], + onAmountChange: (value) => {}, + }, })); sections.push({ @@ -401,7 +405,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [transaction?.splitShares, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -920,8 +924,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onAddToSelection={selectParticipant} onConfirmSelection={confirm} selectedOptions={selectedOptions} - disableArrowKeysActions - boldStyle showTitleTooltip shouldTextInputAppearBelowOptions shouldShowTextInput={false} @@ -1007,4 +1009,5 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, })(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 90ccff47b2b9..acd4c8a9d4d2 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -15,12 +15,14 @@ import DisplayNames from './DisplayNames'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import MoneyRequestAmountInput from './MoneyRequestAmountInput'; import MultipleAvatars from './MultipleAvatars'; import OfflineWithFeedback from './OfflineWithFeedback'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SelectCircle from './SelectCircle'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; +import TextInput from './TextInput'; type OptionRowProps = { /** Style for hovered state */ @@ -251,6 +253,11 @@ function OptionRow({ {option.descriptiveText} ) : null} + {option.amountProps ? ( + + + + ) : null} {!isSelected && option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( { + // TODO: figure out padding for single and 3 character currencies + return 10; switch (prefix) { + case CONST.CURRENCY.USD: case CONST.POLICY.ROOM_PREFIX: return 10; default: @@ -342,7 +346,7 @@ function BaseTextInput( {prefixCharacter} From e9cc173fd234c82a6623df60e766e710aad02153 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 17 Apr 2024 23:50:51 +0100 Subject: [PATCH 055/617] Pass custom styles --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 3f3826d14ca3..973028ada3dd 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -378,12 +378,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participantOption, descriptiveText: null, amountProps: { - value: transaction?.splitShares?.[participantOption.accountID]?.amount, + amount: transaction?.splitShares?.[participantOption.accountID]?.amount, currency: iouCurrencyCode, prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - style: [{minWidth: 100}], + inputStyle: [{width: 100}], + textInputContainerStyles: [], + prefixStyle: [{paddingTop: 0, paddingBottom: 0}], onAmountChange: (value) => {}, }, })); From 7574fc9acbe20a012b7d92a90ff1523d20b373d6 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 13:19:08 +0100 Subject: [PATCH 056/617] add taxAmount param to updateMoneyRequestAmountAndCurrency --- src/libs/actions/IOU.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 83caa65e1d77..43cc73df9c97 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4449,6 +4449,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { transactionThreadReportID: string; currency: string; amount: number; + taxAmount?: number; policy?: OnyxEntry; policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; @@ -4460,6 +4461,7 @@ function updateMoneyRequestAmountAndCurrency({ transactionThreadReportID, currency, amount, + taxAmount, policy, policyTagList, policyCategories, @@ -4467,6 +4469,7 @@ function updateMoneyRequestAmountAndCurrency({ const transactionChanges = { amount, currency, + ...(taxAmount && {taxAmount}), }; const {params, onyxData} = getUpdateMoneyRequestParams( transactionID, From 1ed12dfe94efdb4cf61a0cf865e98b2dd08f71ee Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 13:19:58 +0100 Subject: [PATCH 057/617] should optimistically update tax amount when updating expense amount --- .../iou/request/step/IOURequestStepAmount.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index cb8e51120f01..7b56049811fc 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -16,7 +16,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Transaction} from '@src/types/onyx'; +import type {Policy, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -33,6 +33,9 @@ type IOURequestStepAmountOnyxProps = { /** The draft transaction object being modified in Onyx */ draftTransaction: OnyxEntry; + + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; }; type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & @@ -40,7 +43,15 @@ type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & /** The transaction object being modified in Onyx */ transaction: OnyxEntry; }; - +function getTaxAmount(transaction: OnyxEntry, taxRates: TaxRatesWithDefault | undefined, newAmount: number) { + if (!transaction?.amount) { + return; + } + const transactionTaxCode = transaction?.taxCode ?? ''; + const defaultTaxValue = taxRates?.defaultValue; + const taxPercentage = (transactionTaxCode ? taxRates?.taxes[transactionTaxCode]?.value : defaultTaxValue) ?? ''; + return CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); +} function IOURequestStepAmount({ report, route: { @@ -49,6 +60,7 @@ function IOURequestStepAmount({ transaction, splitDraftTransaction, draftTransaction, + policy, }: IOURequestStepAmountProps) { const {translate} = useLocalize(); const textInput = useRef(null); @@ -148,7 +160,9 @@ function IOURequestStepAmount({ return; } - IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount}); + const taxAmount = getTaxAmount(transaction, policy?.taxRates, newAmount); + + IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount}); Navigation.dismissModal(); }; @@ -190,6 +204,9 @@ export default withWritableReportOrNotFound( return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`; }, }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, })(IOURequestStepAmount), ), ); From 1c16b1aeab10ab4e85462dcc9b50cb399d6588f7 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 13:33:58 +0100 Subject: [PATCH 058/617] add UpdateMoneyRequestTaxRateParams types to updateMoneyRequestTaxRate --- src/libs/actions/IOU.ts | 18 ++++++++++-------- .../request/step/IOURequestStepTaxRatePage.tsx | 9 ++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 43cc73df9c97..04dd151506bb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2379,15 +2379,17 @@ function updateMoneyRequestTaxAmount( API.write('UpdateMoneyRequestTaxAmount', params, onyxData); } +type UpdateMoneyRequestTaxRateParams = { + transactionID: string; + optimisticReportActionID: string; + taxCode: string; + policy: OnyxEntry; + policyTagList: OnyxEntry; + policyCategories: OnyxEntry; +}; + /** Updates the created tax rate of an expense */ -function updateMoneyRequestTaxRate( - transactionID: string, - optimisticReportActionID: string, - taxCode: string, - policy: OnyxEntry, - policyTagList: OnyxEntry, - policyCategories: OnyxEntry, -) { +function updateMoneyRequestTaxRate({transactionID, optimisticReportActionID, taxCode, policy, policyTagList, policyCategories}: UpdateMoneyRequestTaxRateParams) { const transactionChanges = { taxCode, }; diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index da3a244a2db2..3e14610652df 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -74,7 +74,14 @@ function IOURequestStepTaxRatePage({ navigateBack(); return; } - IOU.updateMoneyRequestTaxRate(transaction?.transactionID ?? '', report?.reportID ?? '', newTaxCode, policy, policyTags, policyCategories); + IOU.updateMoneyRequestTaxRate({ + transactionID: transaction?.transactionID ?? '', + optimisticReportActionID: report?.reportID ?? '', + taxCode: newTaxCode, + policy, + policyTagList: policyTags, + policyCategories, + }); navigateBack(); return; } From 8dce58a29da1500c7178168f31affbb669abeb4a Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 13:37:51 +0100 Subject: [PATCH 059/617] add taxAmount to UpdateMoneyRequestTaxRateParams types --- src/libs/actions/IOU.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 04dd151506bb..53fc9e408a06 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2383,15 +2383,17 @@ type UpdateMoneyRequestTaxRateParams = { transactionID: string; optimisticReportActionID: string; taxCode: string; + taxAmount?: number; policy: OnyxEntry; policyTagList: OnyxEntry; policyCategories: OnyxEntry; }; /** Updates the created tax rate of an expense */ -function updateMoneyRequestTaxRate({transactionID, optimisticReportActionID, taxCode, policy, policyTagList, policyCategories}: UpdateMoneyRequestTaxRateParams) { +function updateMoneyRequestTaxRate({transactionID, optimisticReportActionID, taxCode, taxAmount, policy, policyTagList, policyCategories}: UpdateMoneyRequestTaxRateParams) { const transactionChanges = { taxCode, + ...(taxAmount && {taxAmount}), }; const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true); API.write('UpdateMoneyRequestTaxRate', params, onyxData); From 228c47705cc4a06d8d0a343be222e8e952523039 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 14:36:36 +0100 Subject: [PATCH 060/617] should optimistically update tax amount when updating tax rate --- .../iou/request/step/IOURequestStepTaxRatePage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 3e14610652df..55d43370c477 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -68,6 +68,12 @@ function IOURequestStepTaxRatePage({ : transactionTaxCode && TransactionUtils.getTaxName(taxRates.taxes, transactionTaxCode)); const updateTaxRates = (taxes: OptionsListUtils.TaxRatesOption) => { + if (!transaction || !taxes.text || !taxRates) { + Navigation.goBack(backTo); + return; + } + const taxAmount = getTaxAmount(taxRates, taxes.text, TransactionUtils.getAmount(transaction, false, true)); + if (isEditing) { const newTaxCode = taxes.data.code; if (newTaxCode === undefined || newTaxCode === TransactionUtils.getTaxCode(transaction)) { @@ -78,6 +84,7 @@ function IOURequestStepTaxRatePage({ transactionID: transaction?.transactionID ?? '', optimisticReportActionID: report?.reportID ?? '', taxCode: newTaxCode, + taxAmount: CurrencyUtils.convertToBackendAmount(taxAmount ?? 0), policy, policyTagList: policyTags, policyCategories, @@ -85,11 +92,7 @@ function IOURequestStepTaxRatePage({ navigateBack(); return; } - if (!transaction || !taxes.text || !taxRates) { - Navigation.goBack(backTo); - return; - } - const taxAmount = getTaxAmount(taxRates, taxes.text, transaction?.amount); + if (taxAmount === undefined) { Navigation.goBack(backTo); return; From a0ac9bac20da8c89b5a5f2e9bbaee1e314e7bb49 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 18 Apr 2024 14:39:33 +0100 Subject: [PATCH 061/617] fix lint --- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 55d43370c477..c310e88dc928 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -73,7 +73,7 @@ function IOURequestStepTaxRatePage({ return; } const taxAmount = getTaxAmount(taxRates, taxes.text, TransactionUtils.getAmount(transaction, false, true)); - + if (isEditing) { const newTaxCode = taxes.data.code; if (newTaxCode === undefined || newTaxCode === TransactionUtils.getTaxCode(transaction)) { From b24ff87479e0bc8818fe41241abb8b55bbcde871 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 17:04:18 +0100 Subject: [PATCH 062/617] Temporary updates to the amount input --- src/components/AmountTextInput.tsx | 18 +++--------------- ...oraryForRefactorRequestConfirmationList.tsx | 3 --- src/components/OptionRow.tsx | 6 +----- .../TextInput/BaseTextInput/index.tsx | 6 ++++-- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index ff20bbb79ac2..abdef6707327 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -34,19 +34,7 @@ type AmountTextInputProps = { } & Pick; function AmountTextInput( - { - formattedAmount, - onChangeAmount, - placeholder, - selection, - onSelectionChange, - style, - touchableInputWrapperStyle = null, - inputStyle = null, - textInputContainerStyles, - onKeyPress, - ...rest - }: AmountTextInputProps, + {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, ...rest}: AmountTextInputProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -56,8 +44,8 @@ function AmountTextInput( autoGrow hideFocusedState shouldInterceptSwipe - inputStyle={inputStyle ?? [styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} - textInputContainerStyles={textInputContainerStyles ?? [styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} + inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} + textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} onChangeText={onChangeAmount} ref={ref} value={formattedAmount} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 973028ada3dd..f40560be3f5a 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -383,9 +383,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - inputStyle: [{width: 100}], - textInputContainerStyles: [], - prefixStyle: [{paddingTop: 0, paddingBottom: 0}], onAmountChange: (value) => {}, }, })); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index acd4c8a9d4d2..2827f4fa7173 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -253,11 +253,7 @@ function OptionRow({ {option.descriptiveText} ) : null} - {option.amountProps ? ( - - - - ) : null} + {option.amountProps ? : null} {!isSelected && option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, - autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, + autoGrow && StyleUtils.getWidthStyle(autoGrowWidth), + {minWidth: autoGrowWidth}, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); @@ -346,7 +348,7 @@ function BaseTextInput( {prefixCharacter} From 9e53204f30426c4ac5067e8b145f5a2e2b958676 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:36:09 +0100 Subject: [PATCH 063/617] Allow setting individual shares --- src/components/MoneyRequestAmountInput.tsx | 4 +++- ...poraryForRefactorRequestConfirmationList.tsx | 17 +++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 806c31b1c499..7d170f78d073 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -53,6 +53,7 @@ function MoneyRequestAmountTextInput( isCurrencyPressable = true, onCurrencyButtonPress, prefixCharacter, + onAmountChange, hideCurrencySymbol, inputStyle = null, textInputContainerStyles = null, @@ -123,10 +124,11 @@ function MoneyRequestAmountTextInput( hasSelectionBeenSet = true; setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); } + onAmountChange?.(strippedAmount); return strippedAmount; }); }, - [decimals, formError], + [decimals, formError, onAmountChange], ); useEffect(() => {}); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 6610b9fda2e2..be47497512d1 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -115,9 +115,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Payee of the expense with login */ payeePersonalDetails?: OnyxTypes.PersonalDetails; - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - /** Should the list be read only, and not editable? */ isReadOnly?: boolean; @@ -374,11 +371,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const onSplitShareChange = useCallback( + (accountID: number, value) => { + const amountInCents = CurrencyUtils.convertToBackendAmount(value); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents, iouCurrencyCode, true); + }, + [transaction, iouCurrencyCode], + ); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ ...participantOption, descriptiveText: null, amountProps: { @@ -387,7 +392,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - onAmountChange: (value) => {}, + onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), }, })); @@ -408,7 +413,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [transaction?.splitShares, onSplitShareChange, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { From 00db6ffcc34d024c820d98426f403282ef4eaa9e Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:52:34 +0100 Subject: [PATCH 064/617] Show a form error if sum of shares don't match total --- ...oraryForRefactorRequestConfirmationList.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index be47497512d1..f001e3c6452c 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -36,6 +36,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {SplitShare, SplitShares} from '@src/types/onyx/Transaction'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; @@ -379,6 +380,23 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ [transaction, iouCurrencyCode], ); + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const splitSharesMap: SplitShares = transaction.splitShares; + const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); + const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); + if (sumOfShares !== iouAmount) { + setFormError( + `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, + ); + } else { + setFormError(''); + } + }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { From 23d8693339a4ae3078f3d93051bf042c11f24b4e Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:59:21 +0100 Subject: [PATCH 065/617] Prevent submitting form if total doesn't match --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index f001e3c6452c..9b1db9f52b69 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -550,6 +550,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } + if (formError) { + return; + } + playSound(SOUNDS.DONE); setDidConfirm(true); onConfirm?.(selectedParticipants); @@ -568,6 +572,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isDistanceRequestWithPendingRoute, iouAmount, isEditingSplitBill, + formError, onConfirm, ], ); From cc3de1411c101acd7eba3171f570845b1e615b17 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 20:25:11 +0100 Subject: [PATCH 066/617] Adjust shares automatically --- ...raryForRefactorRequestConfirmationList.tsx | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 9b1db9f52b69..93f1a6e4310d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -7,6 +7,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -36,7 +37,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type {SplitShare, SplitShares} from '@src/types/onyx/Transaction'; +import type {SplitShares} from '@src/types/onyx/Transaction'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; @@ -373,11 +374,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); const onSplitShareChange = useCallback( - (accountID: number, value) => { + (accountID: number, value: number) => { + if (!transaction?.transactionID) { + return; + } const amountInCents = CurrencyUtils.convertToBackendAmount(value); - IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents, iouCurrencyCode, true); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); }, - [transaction, iouCurrencyCode], + [transaction?.transactionID], ); useEffect(() => { @@ -397,6 +401,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const sumOfManualShares = Object.keys(transaction.splitShares) + .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) + .reduce((prev: number, current: number): number => prev + current, 0); + + if (!sumOfManualShares) { + return; + } + + const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) + .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string) => Number(key)); + + const remainingTotal = iouAmount - sumOfManualShares; + if (remainingTotal <= 0) { + return; + } + IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); + }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { @@ -405,7 +434,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participantOption, descriptiveText: null, amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID]?.amount, + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, currency: iouCurrencyCode, prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, From d98103f6e391d109bcbe5e39996180296c9dbae0 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 20:27:24 +0100 Subject: [PATCH 067/617] Revert changes --- src/components/TextInput/BaseTextInput/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 36214711287f..519a52fd85ec 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -59,7 +59,6 @@ function BaseTextInput( shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter = '', - prefixStyle = [], inputID, isMarkdownEnabled = false, ...inputProps @@ -242,10 +241,7 @@ function BaseTextInput( // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. const getCharacterPadding = (prefix: string): number => { - // TODO: figure out padding for single and 3 character currencies - return 10; switch (prefix) { - case CONST.CURRENCY.USD: case CONST.POLICY.ROOM_PREFIX: return 10; default: @@ -260,13 +256,11 @@ function BaseTextInput( const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const autoGrowWidth = textInputWidth + getCharacterPadding(prefixCharacter) + 15; const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, - autoGrow && StyleUtils.getWidthStyle(autoGrowWidth), - {minWidth: autoGrowWidth}, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); From 14c9ead507b1b93d5e9734f8cb74e9e3dd481f6f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 21:57:07 +0100 Subject: [PATCH 068/617] Use temporary input --- ...emporaryForRefactorRequestConfirmationList.tsx | 7 ++++++- src/components/TextInput/BaseTextInput/index.tsx | 15 +-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 93f1a6e4310d..896ad130bca9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -11,6 +11,7 @@ import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -211,6 +212,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canUseViolations} = usePermissions(); @@ -430,15 +432,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ ...participantOption, descriptiveText: null, amountProps: { amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, currency: iouCurrencyCode, - prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, + prefixCharacter: currencySymbol, isCurrencyPressable: false, hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), }, })); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 519a52fd85ec..5d1fa4e63742 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -234,20 +234,7 @@ function BaseTextInput( setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); - // When adding a new prefix character, adjust this method to add expected character width. - // This is because character width isn't known before it's rendered to the screen, and once it's rendered, - // it's too late to calculate it's width because the change in padding would cause a visible jump. - // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size - // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, - // this method will produce reliable results. - const getCharacterPadding = (prefix: string): number => { - switch (prefix) { - case CONST.POLICY.ROOM_PREFIX: - return 10; - default: - throw new Error(`Prefix ${prefix} has no padding assigned.`); - } - }; + const getCharacterPadding = (prefix: string): number => prefix.length * 10; const hasLabel = Boolean(label?.length); const isReadOnly = inputProps.readOnly ?? inputProps.disabled; From f032df020db2fdd9452cbff4a5b259f3b8f019f2 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 22:10:20 +0100 Subject: [PATCH 069/617] Show even splits for workspace chat splits --- ...raryForRefactorRequestConfirmationList.tsx | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 896ad130bca9..1e86d0598d17 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -212,7 +212,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canUseViolations} = usePermissions(); @@ -428,26 +427,38 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const getParticipantOptions = useCallback(() => { + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + if (isPolicyExpenseChat) { + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { + const isPayer = participantOption.accountID === payeeOption.accountID; + const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); + return { + ...participantOption, + descriptiveText: CurrencyUtils.convertToDisplayString(amount), + }; + }); + } + + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ + ...participantOption, + amountProps: { + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencySymbol, + isCurrencyPressable: false, + hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], + onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), + }, + })); + }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, personalDetailsOfPayee, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { - const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; - - const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ - ...participantOption, - descriptiveText: null, - amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, - currency: iouCurrencyCode, - prefixCharacter: currencySymbol, - isCurrencyPressable: false, - hideCurrencySymbol: true, - textInputContainerStyles: [{minWidth: 50}], - onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), - }, - })); - + const formattedParticipantsList = getParticipantOptions(); sections.push({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, @@ -465,7 +476,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, onSplitShareChange, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { From 3d2e12e0d5ac3769838d4f70f96a1271b90c5c15 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 22:38:57 +0100 Subject: [PATCH 070/617] Send uneven splits to backend --- src/libs/actions/IOU.ts | 12 ++++++++++-- src/pages/iou/request/step/IOURequestStepAmount.tsx | 1 + .../iou/request/step/IOURequestStepConfirmation.tsx | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 10dc1b44fde5..37daebcd01d6 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3143,6 +3143,7 @@ function createSplitsAndOnyxData( created: string, category: string, tag: string, + splitShares: SplitShares = {}, existingSplitChatReportID = '', billable = false, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, @@ -3311,13 +3312,15 @@ function createSplitsAndOnyxData( } // Loop through participants creating individual chats, iouReports and reportActionIDs as needed - const splitAmount = IOUUtils.calculateAmount(participants.length, amount, currency, false); - const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: IOUUtils.calculateAmount(participants.length, amount, currency, true)}]; + const currentUserAmount = isOwnPolicyExpenseChat ? IOUUtils.calculateAmount(participants.length, amount, currency, true) : splitShares[currentUserAccountID].amount; + + const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: currentUserAmount}]; const hasMultipleParticipants = participants.length > 1; participants.forEach((participant) => { // In a case when a participant is a workspace, even when a current user is not an owner of the workspace const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant); + const splitAmount = isPolicyExpenseChat ? IOUUtils.calculateAmount(participants.length, amount, currency, false) : splitShares[participant.accountID ?? 0].amount; // In case the participant is a workspace, email & accountID should remain undefined and won't be used in the rest of this code // participant.login is undefined when the request is initiated from a group DM with an unknown user, so we need to add a default @@ -3507,6 +3510,7 @@ type SplitBillActionsParams = { billable?: boolean; iouRequestType?: IOURequestType; existingSplitChatReportID?: string; + splitShares?: SplitShares; }; /** @@ -3527,6 +3531,7 @@ function splitBill({ billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, existingSplitChatReportID = '', + splitShares = {}, }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3540,6 +3545,7 @@ function splitBill({ currentCreated, category, tag, + splitShares, existingSplitChatReportID, billable, iouRequestType, @@ -3586,6 +3592,7 @@ function splitBillAndOpenReport({ tag = '', billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + splitShares = {}, }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3599,6 +3606,7 @@ function splitBillAndOpenReport({ currentCreated, category, tag, + splitShares, '', billable, iouRequestType, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 752f291642cd..12dc29418e93 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -170,6 +170,7 @@ function IOURequestStepAmount({ created: transaction?.created ?? '', billable: false, iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + splitShares: transaction?.splitShares, }); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1d84bff8747b..71b0d369912f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -326,6 +326,7 @@ function IOURequestStepConfirmation({ existingSplitChatReportID: report?.reportID, billable: transaction.billable, iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, }); } return; @@ -347,6 +348,7 @@ function IOURequestStepConfirmation({ tag: transaction.tag, billable: !!transaction.billable, iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, }); } return; From eb5c8f12ad5776ef8c5e99252b27fe1eab05938a Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 23:12:24 +0100 Subject: [PATCH 071/617] Save splits array in the transaction optimistically --- src/libs/actions/IOU.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 37daebcd01d6..fc200a8768f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3477,6 +3477,16 @@ function createSplitsAndOnyxData( failureData.push(...oneOnOneFailureData); }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, + value: { + comment: { + splits: splits.map((split) => ({accountID: split.accountID, amount: split.amount})), + }, + }, + }); + const splitData: SplitData = { chatReportID: splitChatReport.reportID, transactionID: splitTransaction.transactionID, From 3ba09e8dd2e48269392a56fa39919315e0a75b1e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Fri, 19 Apr 2024 10:03:47 +0100 Subject: [PATCH 072/617] fix lint --- src/pages/iou/request/step/IOURequestStepAmount.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 972f9972aaea..e612d04eb733 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -20,7 +20,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {TaxRatesWithDefault, Transaction} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; @@ -56,7 +55,7 @@ type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & /** The transaction object being modified in Onyx */ transaction: OnyxEntry; }; -function getTaxAmount(transaction: OnyxEntry, taxRates: TaxRatesWithDefault | undefined, newAmount: number) { +function getTaxAmount(transaction: OnyxEntry, taxRates: OnyxTypes.TaxRatesWithDefault | undefined, newAmount: number) { if (!transaction?.amount) { return; } From 88a10fa97fe45013ea8ab916f4e62d17c18db4ce Mon Sep 17 00:00:00 2001 From: GandalfGwaihir Date: Fri, 19 Apr 2024 14:41:19 +0530 Subject: [PATCH 073/617] Fix background and foreground color for report headers --- src/components/ReportHeaderSkeletonView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportHeaderSkeletonView.tsx b/src/components/ReportHeaderSkeletonView.tsx index bc4eef675170..3a94516b2c29 100644 --- a/src/components/ReportHeaderSkeletonView.tsx +++ b/src/components/ReportHeaderSkeletonView.tsx @@ -48,8 +48,8 @@ function ReportHeaderSkeletonView({shouldAnimate = true, onBackButtonPress = () animate={shouldAnimate} width={styles.w100.width} height={height} - backgroundColor={theme.highlightBG} - foregroundColor={theme.border} + backgroundColor={theme.skeletonLHNIn} + foregroundColor={theme.skeletonLHNOut} > Date: Fri, 19 Apr 2024 23:16:50 +0100 Subject: [PATCH 074/617] Copy changes to MoneyRequestConfirmationList --- .../MoneyRequestConfirmationList.tsx | 158 ++- ...raryForRefactorRequestConfirmationList.tsx | 1047 ----------------- 2 files changed, 95 insertions(+), 1110 deletions(-) delete mode 100755 src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e309df1ab654..a146332f80c8 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -37,6 +37,7 @@ import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {SplitShares} from '@src/types/onyx/Transaction'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; @@ -121,9 +122,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Payee of the expense with login */ payeePersonalDetails?: OnyxEntry; - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - /** Should the list be read only, and not editable? */ isReadOnly?: boolean; @@ -171,6 +169,8 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** The action to take */ action?: IOUAction; + + currencyList: OnyxEntry; }; const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { @@ -200,7 +200,6 @@ function MoneyRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, - canModifyParticipants: canModifyParticipantsProp = false, session, isReadOnly = false, bankAccountRoute = '', @@ -218,6 +217,7 @@ function MoneyRequestConfirmationList({ defaultMileageRate, lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, + currencyList, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -374,17 +374,6 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); }, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]); - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); - }, - [iouAmount, iouCurrencyCode], - ); - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); @@ -413,43 +402,98 @@ function MoneyRequestConfirmationList({ ]; }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); + const onSplitShareChange = useCallback( + (accountID: number, value: number) => { + if (!transaction?.transactionID) { + return; + } + const amountInCents = CurrencyUtils.convertToBackendAmount(value); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); + }, + [transaction?.transactionID], + ); + + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const splitSharesMap: SplitShares = transaction.splitShares; + const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); + const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); + if (sumOfShares !== iouAmount) { + setFormError( + `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, + ); + } else { + setFormError(''); + } + }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const sumOfManualShares = Object.keys(transaction.splitShares) + .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) + .reduce((prev: number, current: number): number => prev + current, 0); + + if (!sumOfManualShares) { + return; + } + + const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) + .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string) => Number(key)); + + const remainingTotal = iouAmount - sumOfManualShares; + if (remainingTotal <= 0) { + return; + } + IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); + }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); - const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; + const getParticipantOptions = useCallback(() => { + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails); + if (isPolicyExpenseChat) { + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { + const isPayer = participantOption.accountID === payeeOption.accountID; + const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); + return { + ...participantOption, + descriptiveText: CurrencyUtils.convertToDisplayString(amount), + }; + }); + } + + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ + ...participantOption, + amountProps: { + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencySymbol, + isCurrencyPressable: false, + hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], + onAmountChange: (value: string) => onSplitShareChange(participantOption.accountID ?? 0, Number(value)), + }, + })); + }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, payeePersonalDetails, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); + const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipants) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } - - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + const formattedParticipantsList = getParticipantOptions(); + sections.push({ + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -462,18 +506,7 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - selectedParticipantsProp, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - payeePersonalDetails, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); + }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -982,18 +1015,16 @@ function MoneyRequestConfirmationList({ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) ; - - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; - - /** The policy of the report */ - policy: OnyxEntry; - - /** The session of the logged in user */ - session: OnyxEntry; - - /** Unit and rate used for if the expense is a distance expense */ - mileageRate: OnyxEntry; -}; - -type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { - /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Participant[]) => void; - - /** Callback to parent modal to pay someone */ - onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; - - /** Callback to inform a participant is selected */ - onSelectParticipant?: (option: Participant) => void; - - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: boolean; - - /** IOU amount */ - iouAmount: number; - - /** IOU comment */ - iouComment?: string; - - /** IOU currency */ - iouCurrencyCode?: string; - - /** IOU type */ - iouType?: IOUType; - - /** IOU date */ - iouCreated?: string; - - /** IOU merchant */ - iouMerchant?: string; - - /** IOU Category */ - iouCategory?: string; - - /** IOU isBillable */ - iouIsBillable?: boolean; - - /** Callback to toggle the billable state */ - onToggleBillable?: (isOn: boolean) => void; - - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Participant[]; - - /** Payee of the expense with login */ - payeePersonalDetails?: OnyxTypes.PersonalDetails; - - /** Should the list be read only, and not editable? */ - isReadOnly?: boolean; - - /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute?: Route; - - /** The policyID of the request */ - policyID?: string; - - /** The reportID of the request */ - reportID?: string; - - /** File path of the receipt */ - receiptPath?: string; - - /** File name of the receipt */ - receiptFilename?: string; - - /** List styles for OptionsSelector */ - listStyles?: StyleProp; - - /** Transaction that represents the expense */ - transaction?: OnyxEntry; - - /** Whether the expense is a distance expense */ - isDistanceRequest?: boolean; - - /** Whether the expense is a scan expense */ - isScanRequest?: boolean; - - /** Whether we're editing a split expense */ - isEditingSplitBill?: boolean; - - /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields?: boolean; - - /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat?: boolean; - - /** Whether smart scan failed */ - hasSmartScanFailed?: boolean; - - reportActionID?: string; - - action?: IOUAction; -}; - -const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || ''; - return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); -}; - -function MoneyTemporaryForRefactorRequestConfirmationList({ - transaction = null, - onSendMoney, - onConfirm, - onSelectParticipant, - iouType = CONST.IOU.TYPE.REQUEST, - isScanRequest = false, - iouAmount, - policyCategories, - mileageRate, - isDistanceRequest = false, - policy, - isPolicyExpenseChat = false, - iouCategory = '', - shouldShowSmartScanFields = true, - isEditingSplitBill, - policyTags, - iouCurrencyCode, - iouMerchant, - hasMultipleParticipants, - selectedParticipants: pickedParticipants, - payeePersonalDetails, - currencyList, - session, - isReadOnly = false, - bankAccountRoute = '', - policyID = '', - reportID = '', - receiptPath = '', - iouComment, - receiptFilename = '', - listStyles, - iouCreated, - iouIsBillable = false, - onToggleBillable, - hasSmartScanFailed, - reportActionID, - action = CONST.IOU.ACTION.CREATE, -}: MoneyRequestConfirmationListProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate, toLocaleDigit} = useLocalize(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {canUseViolations} = usePermissions(); - - const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; - - const {unit, rate, currency} = mileageRate ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - rate: 0, - currency: 'USD', - }; - const distance = transaction?.routes?.route0.distance ?? 0; - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; - const taxRates = policy?.taxRates; - - // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - - // A flag and a toggler for showing the rest of the form fields - const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - - // Do not hide fields in case of paying someone - const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - - const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; - const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; - - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - - // A flag for showing the tags field - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); - - // A flag for showing tax rate - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); - - // A flag for showing the billable field - const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; - const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); - const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; - const formattedAmount = isDistanceRequestWithPendingRoute - ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, - isDistanceRequest ? currency : iouCurrencyCode, - ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; - - const previousTransactionAmount = usePrevious(transaction?.amount); - - const isFocused = useIsFocused(); - const [formError, setFormError] = useState(''); - - const [didConfirm, setDidConfirm] = useState(false); - const [didConfirmSplit, setDidConfirmSplit] = useState(false); - - const [merchantError, setMerchantError] = useState(false); - - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - - const navigateBack = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID)); - }; - - const shouldDisplayFieldError: boolean = useMemo(() => { - if (!isEditingSplitBill) { - return false; - } - - return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - - const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; - - const isCategoryRequired = canUseViolations && !!policy?.requiresCategory; - - useEffect(() => { - if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { - return; - } - if (!isMerchantEmpty && merchantError) { - setMerchantError(false); - if (formError === 'iou.error.invalidMerchant') { - setFormError(''); - } - } - }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); - - useEffect(() => { - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (merchantError) { - setFormError('iou.error.invalidMerchant'); - return; - } - // reset the form error whenever the screen gains or loses focus - setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); - - useEffect(() => { - if (!shouldCalculateDistanceAmount) { - return; - } - - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); - IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? ''); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); - - // Calculate and set tax amount in transaction draft - useEffect(() => { - const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); - const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - - if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { - return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); - } - - IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true); - }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); - - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again - if (isEditingSplitBill && didConfirm) { - setDidConfirm(false); - } - - const splitOrRequestOptions: Array> = useMemo(() => { - let text; - if (isTypeTrackExpense) { - text = translate('iou.trackExpense'); - } else if (isTypeSplit && iouAmount === 0) { - text = translate('iou.splitExpense'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.submitExpense'); - if (iouAmount !== 0) { - text = translate('iou.submitAmount', {amount: formattedAmount}); - } - } else { - const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; - text = translate(translationKey, {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: iouType, - }, - ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - - const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); - const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - - const onSplitShareChange = useCallback( - (accountID: number, value: number) => { - if (!transaction?.transactionID) { - return; - } - const amountInCents = CurrencyUtils.convertToBackendAmount(value); - IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); - }, - [transaction?.transactionID], - ); - - useEffect(() => { - if (!isTypeSplit || !transaction?.splitShares) { - return; - } - - const splitSharesMap: SplitShares = transaction.splitShares; - const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); - const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); - if (sumOfShares !== iouAmount) { - setFormError( - `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, - ); - } else { - setFormError(''); - } - }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); - - useEffect(() => { - if (!isTypeSplit || !transaction?.splitShares) { - return; - } - - const sumOfManualShares = Object.keys(transaction.splitShares) - .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) - .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) - .reduce((prev: number, current: number): number => prev + current, 0); - - if (!sumOfManualShares) { - return; - } - - const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) - .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) - .map((key: string) => Number(key)); - - const remainingTotal = iouAmount - sumOfManualShares; - if (remainingTotal <= 0) { - return; - } - IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); - }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); - - const getParticipantOptions = useCallback(() => { - const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - if (isPolicyExpenseChat) { - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { - const isPayer = participantOption.accountID === payeeOption.accountID; - const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); - return { - ...participantOption, - descriptiveText: CurrencyUtils.convertToDisplayString(amount), - }; - }); - } - - const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ - ...participantOption, - amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, - currency: iouCurrencyCode, - prefixCharacter: currencySymbol, - isCurrencyPressable: false, - hideCurrencySymbol: true, - textInputContainerStyles: [{minWidth: 50}], - onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), - }, - })); - }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, personalDetailsOfPayee, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); - - const optionSelectorSections = useMemo(() => { - const sections = []; - if (hasMultipleParticipants) { - const formattedParticipantsList = getParticipantOptions(); - sections.push({ - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }); - } else { - const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, - isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - sections.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - shouldShow: true, - }); - } - return sections; - }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); - - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)]; - }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]); - - useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { - return; - } - - /* - Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: - When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. - In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. - */ - IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction, action, isMovingTransactionFromTrackExpense]); - - // Auto select the category if there is only one enabled category and it is required - useEffect(() => { - const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); - if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { - return; - } - IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldShowCategories, policyCategories, isCategoryRequired]); - - // Auto select the tag if there is only one enabled tag and it is required - useEffect(() => { - let updatedTagsString = TransactionUtils.getTag(transaction); - policyTagLists.forEach((tagList, index) => { - const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled); - const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations; - if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { - return; - } - updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); - }); - if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { - IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [policyTagLists, policyTags, canUseViolations]); - - /** - */ - const selectParticipant = useCallback( - (option: Participant) => { - // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID) { - return; - } - onSelectParticipant?.(option); - }, - [session?.accountID, onSelectParticipant], - ); - - /** - * Navigate to report details or profile of selected user - */ - const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - if (option.isSelfDM) { - Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); - return; - } - - if (option.accountID) { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); - } - }; - - /** - * @param {String} paymentMethod - */ - const confirm = useCallback( - (paymentMethod: PaymentMethodType | undefined) => { - if (selectedParticipants.length === 0) { - return; - } - if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) { - setMerchantError(true); - return; - } - - if (iouType === CONST.IOU.TYPE.SEND) { - if (!paymentMethod) { - return; - } - - setDidConfirm(true); - - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney?.(paymentMethod); - } else { - // validate the amount for distance expenses - const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); - if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { - setFormError('common.error.invalidAmount'); - return; - } - - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { - setDidConfirmSplit(true); - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - - if (formError) { - return; - } - - playSound(SOUNDS.DONE); - setDidConfirm(true); - onConfirm?.(selectedParticipants); - } - }, - [ - selectedParticipants, - isMerchantRequired, - isMerchantEmpty, - shouldDisplayFieldError, - transaction, - iouType, - onSendMoney, - iouCurrencyCode, - isDistanceRequest, - isDistanceRequestWithPendingRoute, - iouAmount, - isEditingSplitBill, - formError, - onConfirm, - ], - ); - - const footerContent = useMemo(() => { - if (isReadOnly) { - return; - } - - const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; - - const button = shouldShowSettlementButton ? ( - - ) : ( - confirm(value as PaymentMethodType)} - options={splitOrRequestOptions} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} - enterKeyEventListenerPriority={1} - /> - ); - - return ( - <> - {!!formError && ( - - )} - - {button} - - ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". - // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. - const classifiedFields = [ - { - item: ( - { - if (isDistanceRequest) { - return; - } - if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} - /> - ), - shouldShow: shouldShowSmartScanFields, - isSupplementary: false, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - ), - shouldShow: true, - isSupplementary: false, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - disabled={didConfirm} - // todo: handle edit for transaction while moving from track expense - interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} - /> - ), - shouldShow: isDistanceRequest, - isSupplementary: true, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={merchantError ? translate('common.error.fieldRequired') : ''} - rightLabel={isMerchantRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowMerchant, - isSupplementary: !isMerchantRequired, - }, - { - item: ( - { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - ), - shouldShow: shouldShowDate, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isCategoryRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowCategories, - isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, - }, - ...policyTagLists.map(({name, required}, index) => { - const isTagRequired = required === undefined ? false : canUseViolations && required; - return { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isTagRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowTags, - isSupplementary: !isTagRequired, - }; - }), - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - {translate('common.billable')} - onToggleBillable?.(isOn)} - /> - - ), - shouldShow: shouldShowBillable, - isSupplementary: true, - }, - ]; - - const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); - - const supplementaryFields = classifiedFields - .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) - .map((supplementaryField) => supplementaryField.item); - - const { - image: receiptImage, - thumbnail: receiptThumbnail, - isThumbnail, - fileExtension, - isLocalFile, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); - - const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); - const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - - const receiptThumbnailContent = useMemo( - () => - isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ), - [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], - ); - - return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - - {isDistanceRequest && ( - - - - )} - {(!isMovingTransactionFromTrackExpense || !hasRoute) && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (receiptImage || receiptThumbnail - ? receiptThumbnailContent - : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - /> - ))} - {primaryFields} - {!shouldShowAllFields && ( - - -